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

560
common/BUILD Normal file
View File

@@ -0,0 +1,560 @@
package(default_visibility = ["//visibility:public"])
cc_library(
name = "buffer",
srcs = ["buffer.cc"],
hdrs = ["buffer.h"],
)
cc_test(
name = "buffer_test",
srcs = ["buffer_test.cc"],
deps = [
":buffer",
"@com_google_googletest//:gtest",
"@com_google_googletest//:gtest_main",
],
)
cc_library(
name = "clock",
srcs = ["clock.cc"],
hdrs = ["clock.h"],
deps = [
":platform",
"@com_google_absl//absl/strings:str_format",
],
)
cc_library(
name = "dir_iter",
srcs = ["dir_iter.cc"],
hdrs = ["dir_iter.h"],
copts = select({
"//tools:windows": [
# Additional warnings from @com_github_dirent
"/wd4505", # unreferenced function with internal linkage has been removed
],
"//conditions:default": ["/wd4505"],
}),
deps = [
":path",
":platform",
"@com_github_dirent//:dirent",
"@com_google_absl//absl/status",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/strings:str_format",
],
)
cc_test(
name = "dir_iter_test",
srcs = ["dir_iter_test.cc"],
data = ["testdata/root.txt"] + glob(["testdata/dir_iter/**"]),
deps = [
":dir_iter",
":status_test_macros",
":test_main",
"@com_google_googletest//:gtest",
],
)
cc_library(
name = "errno_mapping",
srcs = ["errno_mapping.cc"],
hdrs = ["errno_mapping.h"],
deps = [
"@com_google_absl//absl/status",
"@com_google_absl//absl/strings",
],
)
cc_test(
name = "errno_mapping_test",
srcs = ["errno_mapping_test.cc"],
deps = [
":errno_mapping",
":status_test_macros",
"@com_google_googletest//:gtest",
"@com_google_googletest//:gtest_main",
],
)
cc_library(
name = "file_watcher",
srcs = [
"file_watcher_win.cc",
],
hdrs = [
"file_watcher_win.h",
],
# Required for ReadDirectoryChangesExW (requires Win10 1709).
copts = ["/D_WIN32_WINNT=0x0A00"],
target_compatible_with = ["@platforms//os:windows"],
deps = [
":log",
":path",
":platform",
":scoped_handle",
":status",
":stopwatch",
":util",
"@com_google_absl//absl/status",
"@com_google_absl//absl/status:statusor",
"@com_google_absl//absl/strings:str_format",
"@com_google_absl//absl/synchronization",
],
)
cc_test(
name = "file_watcher_test",
srcs = ["file_watcher_win_test.cc"],
target_compatible_with = ["@platforms//os:windows"],
deps = [
":file_watcher",
":path",
":platform",
":status_test_macros",
":util",
"@com_google_googletest//:gtest",
"@com_google_googletest//:gtest_main",
],
)
cc_library(
name = "grpc_status",
hdrs = ["grpc_status.h"],
deps = [
"@com_github_grpc_grpc//:grpc++",
"@com_google_absl//absl/status",
],
)
cc_library(
name = "log",
srcs = ["log.cc"],
hdrs = ["log.h"],
deps = [
":clock",
":platform",
"@com_google_absl//absl/strings:str_format",
"@com_google_absl//absl/synchronization",
],
)
cc_test(
name = "log_test",
srcs = ["log_test.cc"],
deps = [
":log",
":path",
":status_test_macros",
"@com_google_absl//absl/strings",
"@com_google_googletest//:gtest",
"@com_google_googletest//:gtest_main",
],
)
cc_library(
name = "path",
srcs = ["path.cc"],
hdrs = ["path.h"],
linkopts = select({
"//tools:windows": [
"/DEFAULTLIB:ole32.lib", # CoTaskMemFree
"/DEFAULTLIB:shell32.lib", # SHGetKnownFolderPath
],
"//conditions:default": [],
}),
deps = [
":buffer",
":errno_mapping",
":log",
":platform",
":status",
":status_macros",
":util",
"@com_google_absl//absl/status",
"@com_google_absl//absl/status:statusor",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/strings:str_format",
],
)
cc_test(
name = "path_test",
srcs = ["path_test.cc"],
env_inherit = select({
"//tools:windows": [
"ProgramFiles(x86)",
"USERPROFILE",
],
"//conditions:default": [],
}),
deps = [
":path",
":status_test_macros",
":test_main",
":util",
"@com_google_googletest//:gtest",
],
)
cc_library(
name = "path_filter",
srcs = ["path_filter.cc"],
hdrs = ["path_filter.h"],
)
cc_test(
name = "path_filter_test",
srcs = ["path_filter_test.cc"],
deps = [
":path_filter",
":test_main",
"@com_google_googletest//:gtest",
],
)
cc_library(
name = "platform",
hdrs = ["platform.h"],
)
cc_library(
name = "port_manager",
srcs = ["port_manager_win.cc"],
hdrs = ["port_manager.h"],
target_compatible_with = ["@platforms//os:windows"],
deps = [
":remote_util",
":status",
":stopwatch",
":util",
"@com_google_absl//absl/status:statusor",
],
)
cc_test(
name = "port_manager_test",
srcs = ["port_manager_test.cc"],
target_compatible_with = ["@platforms//os:windows"],
deps = [
":port_manager",
":status_test_macros",
":stub_process",
":test_main",
":testing_clock",
"@com_google_googletest//:gtest",
],
)
cc_library(
name = "process",
srcs = ["process_win.cc"],
hdrs = ["process.h"],
target_compatible_with = ["@platforms//os:windows"],
deps = [
":log",
":scoped_handle",
":status",
":status_test_macros",
":stopwatch",
":util",
"@com_google_absl//absl/status",
"@com_google_absl//absl/status:statusor",
"@com_google_absl//absl/strings:str_format",
],
)
cc_test(
name = "process_test",
srcs = ["process_test.cc"],
target_compatible_with = ["@platforms//os:windows"],
deps = [
":process",
":test_main",
"@com_google_googletest//:gtest",
],
)
cc_library(
name = "status_macros",
hdrs = ["status_macros.h"],
deps = [
":status",
"@com_google_absl//absl/status",
"@com_google_absl//absl/status:statusor",
"@com_google_absl//absl/strings:str_format",
],
)
cc_library(
name = "status_test_macros",
hdrs = ["status_test_macros.h"],
deps = [
":platform",
"@com_google_absl//absl/status",
"@com_google_absl//absl/status:statusor",
],
)
cc_library(
name = "stub_process",
srcs = ["stub_process.cc"],
hdrs = ["stub_process.h"],
deps = [
":process",
":status_macros",
],
)
cc_library(
name = "remote_util",
srcs = ["remote_util.cc"],
hdrs = ["remote_util.h"],
deps = [
":platform",
":process",
":sdk_util",
":util",
"@com_google_absl//absl/status",
],
)
cc_test(
name = "remote_util_test",
srcs = ["remote_util_test.cc"],
deps = [
":remote_util",
":test_main",
],
)
cc_library(
name = "semaphore",
srcs = ["semaphore.cc"],
hdrs = ["semaphore.h"],
)
cc_test(
name = "semaphore_test",
srcs = ["semaphore_test.cc"],
deps = [
":semaphore",
":test_main",
"@com_google_googletest//:gtest",
],
)
cc_library(
name = "gamelet_component",
srcs = ["gamelet_component.cc"],
hdrs = ["gamelet_component.h"],
deps = [
":path",
":platform",
":status",
"@com_google_absl//absl/status",
"@com_google_absl//absl/strings:str_format",
],
)
cc_test(
name = "gamelet_component_test",
srcs = ["gamelet_component_test.cc"],
data = ["testdata/root.txt"] + glob(["testdata/gamelet_component/**"]),
deps = [
":gamelet_component",
":status_test_macros",
":test_main",
"@com_google_googletest//:gtest",
],
)
cc_library(
name = "stats_collector",
srcs = ["stats_collector.cc"],
hdrs = ["stats_collector.h"],
deps = [
":log",
":stopwatch",
"@com_google_absl//absl/strings:str_format",
"@com_google_absl//absl/synchronization",
],
)
cc_library(
name = "status",
srcs = ["status.cc"],
hdrs = ["status.h"],
deps = [
":platform",
"@com_google_absl//absl/status",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/strings:str_format",
],
)
cc_library(
name = "stopwatch",
srcs = ["stopwatch.cc"],
hdrs = ["stopwatch.h"],
deps = [
":clock",
"@com_google_absl//absl/time",
],
)
cc_test(
name = "stopwatch_test",
srcs = ["stopwatch_test.cc"],
deps = [
":stopwatch",
":test_main",
":testing_clock",
"@com_google_googletest//:gtest",
],
)
cc_library(
name = "sdk_util",
srcs = ["sdk_util.cc"],
hdrs = ["sdk_util.h"],
target_compatible_with = ["@platforms//os:windows"],
deps = [
":path",
":platform",
":status_macros",
"@com_google_absl//absl/status",
"@com_google_absl//absl/status:statusor",
],
)
cc_test(
name = "sdk_util_test",
srcs = ["sdk_util_test.cc"],
deps = [
":sdk_util",
":status_test_macros",
":test_main",
"@com_google_googletest//:gtest",
],
)
cc_library(
name = "threadpool",
srcs = ["threadpool.cc"],
hdrs = ["threadpool.h"],
deps = ["@com_google_absl//absl/synchronization"],
)
cc_test(
name = "threadpool_test",
srcs = ["threadpool_test.cc"],
deps = [
":semaphore",
":test_main",
":threadpool",
"@com_google_googletest//:gtest",
],
)
cc_library(
name = "testing_clock",
srcs = ["testing_clock.cc"],
hdrs = ["testing_clock.h"],
deps = [
":clock",
":stopwatch",
],
)
cc_library(
name = "test_main",
srcs = ["test_main.cc"],
hdrs = ["test_main.h"],
deps = [
":path",
"@bazel_tools//tools/cpp/runfiles",
"@com_google_googletest//:gtest",
],
)
cc_library(
name = "thread_safe_map",
hdrs = ["thread_safe_map.h"],
deps = ["@com_google_absl//absl/synchronization"],
)
cc_test(
name = "thread_safe_map_test",
srcs = ["thread_safe_map_test.cc"],
deps = [
":thread_safe_map",
"@com_google_googletest//:gtest",
"@com_google_googletest//:gtest_main",
],
)
cc_library(
name = "scoped_handle",
srcs = ["scoped_handle_win.cc"],
hdrs = ["scoped_handle_win.h"],
target_compatible_with = ["@platforms//os:windows"],
)
cc_library(
name = "util",
srcs = ["util.cc"],
hdrs = ["util.h"],
deps = [
":platform",
"@com_google_absl//absl/random",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/strings:str_format",
"@com_google_absl//absl/time",
],
)
cc_test(
name = "util_test",
srcs = ["util_test.cc"],
deps = [
":util",
"@com_google_googletest//:gtest",
"@com_google_googletest//:gtest_main",
],
)
cc_library(
name = "url",
srcs = ["url.cc"],
hdrs = ["url.h"],
deps = [
"@com_google_absl//absl/status:statusor",
"@com_google_absl//absl/strings:str_format",
],
)
cc_test(
name = "url_test",
srcs = ["url_test.cc"],
deps = [
":status_test_macros",
":url",
"@com_google_absl//absl/status:statusor",
"@com_google_googletest//:gtest",
"@com_google_googletest//:gtest_main",
],
)
filegroup(
name = "all_test_sources",
srcs = glob(["*_test.cc"]),
)
filegroup(
name = "all_test_data",
srcs = glob(["testdata/**"]),
)

88
common/buffer.cc Normal file
View File

@@ -0,0 +1,88 @@
// 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 "common/buffer.h"
#include <cstring>
#include <utility>
namespace cdc_ft {
Buffer::Buffer() = default;
Buffer::Buffer(size_t size) : size_(size) {
reserve(size);
resize(size);
}
Buffer::Buffer(std::initializer_list<char> list) {
reserve(list.size());
resize(list.size());
memcpy(data_, list.begin(), list.size());
}
Buffer::Buffer(Buffer&& other) noexcept { *this = std::move(other); }
Buffer& Buffer::operator=(Buffer&& other) noexcept {
if (data_) {
free(data_);
}
size_ = other.size_;
capacity_ = other.capacity_;
data_ = other.data_;
other.size_ = 0;
other.capacity_ = 0;
other.data_ = nullptr;
return *this;
}
Buffer::~Buffer() {
if (data_) {
free(data_);
data_ = nullptr;
capacity_ = 0;
}
}
bool Buffer::operator==(const Buffer& other) const {
return size_ == other.size_ && memcmp(data_, other.data_, size_) == 0;
}
bool Buffer::operator!=(const Buffer& other) const { return !(*this == other); }
void Buffer::resize(size_t new_size) {
size_ = new_size;
if (capacity_ < size_) {
capacity_ = size_ + size_ / 2;
data_ = static_cast<char*>(realloc(data_, capacity_));
}
}
void Buffer::reserve(size_t capacity) {
if (capacity_ < capacity) {
capacity_ = capacity;
data_ = static_cast<char*>(realloc(data_, capacity_));
}
}
void Buffer::append(const void* data, size_t data_size) {
size_t prev_size = size_;
resize(prev_size + data_size);
memcpy(data_ + prev_size, data, data_size);
}
} // namespace cdc_ft

87
common/buffer.h Normal file
View File

@@ -0,0 +1,87 @@
/*
* 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 COMMON_BUFFER_H_
#define COMMON_BUFFER_H_
#include <cstdlib>
#include <initializer_list>
namespace cdc_ft {
// Buffer for generic data blobs. It is a managed and dynamically sized memory
// buffer with an interface similar to that of std::vector. It can be used as an
// in-place replacement in some use cases like reading files.
// The class solves a performance problem on Linux where resize() can be
// relatively expensive since a vector default-constructs elements. This can
// be a big performance bottleneck in some cases.
class Buffer {
public:
Buffer();
explicit Buffer(size_t size);
Buffer(std::initializer_list<char> list);
~Buffer();
// Non-copyable, non-assignable for safety.
Buffer(const Buffer& other) = delete;
Buffer& operator=(const Buffer& other) = delete;
Buffer(Buffer&& other) noexcept;
Buffer& operator=(Buffer&& other) noexcept;
bool operator==(const Buffer& other) const;
bool operator!=(const Buffer& other) const;
// Resizes the buffer. Only reallocates if the size increases. In that case,
// allocates 1.5x more than required. If size shrinks, keeps the buffer.
// Does not initialize the buffer.
void resize(size_t new_size);
// Makes sure the buffer capacity is at least the given number of bytes.
void reserve(size_t capacity);
// Returns the current size of the buffer.
size_t size() const { return size_; }
// Returns true if the size is zero.
bool empty() const { return size_ == 0; }
// Returns the currently allocated buffer size.
size_t capacity() const { return capacity_; }
// Resizes the buffer to 0.
void clear() { size_ = 0; }
// Returns the data pointer.
char* data() { return data_; }
// Returns the data pointer.
const char* data() const { return data_; }
// Appends |data| of size |data_size| to the end of the buffer. The behavior
// is undefined if the provided memory range overlaps with the buffer's data.
void append(const void* data, size_t data_size);
private:
char* data_ = nullptr;
size_t size_ = 0;
size_t capacity_ = 0;
};
} // namespace cdc_ft
#endif // COMMON_BUFFER_H_

167
common/buffer_test.cc Normal file
View File

@@ -0,0 +1,167 @@
// 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 "common/buffer.h"
#include <cstring>
#include "gtest/gtest.h"
namespace cdc_ft {
namespace {
TEST(BufferTest, ConstructDefault) {
Buffer b;
EXPECT_EQ(b.size(), 0);
EXPECT_EQ(b.data(), nullptr);
}
TEST(BufferTest, ConstructWithSize) {
Buffer b(32);
EXPECT_EQ(b.size(), 32);
EXPECT_EQ(b.capacity(), 32);
EXPECT_NE(b.data(), nullptr);
// This shouldn't crash.
memset(b.data(), 0, b.size());
}
TEST(BufferTest, ConstructWithInitializerList) {
Buffer b({'1', '2', '3', '4', '5', '6', '7', '8', '9'});
EXPECT_EQ(b.size(), 9);
EXPECT_EQ(b.capacity(), 9);
EXPECT_NE(b.data(), nullptr);
EXPECT_EQ(memcmp(b.data(), "123456789", 9), 0);
}
TEST(BufferTest, MoveConstructor) {
Buffer b({9});
Buffer b2(std::move(b));
EXPECT_EQ(b.size(), 0);
EXPECT_EQ(b.data(), nullptr);
EXPECT_EQ(b.capacity(), 0);
EXPECT_EQ(b2.size(), 1);
EXPECT_NE(b2.data(), nullptr);
EXPECT_EQ(*b2.data(), 9);
}
TEST(BufferTest, MoveOperator) {
Buffer b({9});
Buffer b2({12, 13});
b2 = std::move(b);
EXPECT_EQ(b.size(), 0);
EXPECT_EQ(b.data(), nullptr);
EXPECT_EQ(b.capacity(), 0);
EXPECT_EQ(b2.size(), 1);
EXPECT_NE(b2.data(), nullptr);
EXPECT_EQ(*b2.data(), 9);
}
TEST(BufferTest, EqualsOperator) {
Buffer b({1});
Buffer b2({1});
Buffer b3({2});
EXPECT_TRUE(b == b2);
EXPECT_FALSE(b == b3);
EXPECT_FALSE(b != b2);
EXPECT_TRUE(b != b3);
}
TEST(BufferTest, SizeIncrease) {
Buffer b(8);
EXPECT_EQ(b.size(), 8);
EXPECT_EQ(b.capacity(), 8);
EXPECT_NE(b.data(), nullptr);
memcpy(b.data(), "01234567", 8);
// Should realloc the buffer, but keep the data.
b.resize(10);
EXPECT_EQ(b.size(), 10);
EXPECT_EQ(b.capacity(), 15);
EXPECT_NE(b.data(), nullptr);
EXPECT_EQ(memcmp(b.data(), "01234567", 8), 0);
memcpy(b.data(), "0123456789", 10);
// Should not realloc the buffer and keep data.
b.resize(12);
EXPECT_EQ(b.size(), 12);
EXPECT_EQ(b.capacity(), 15);
EXPECT_NE(b.data(), nullptr);
EXPECT_EQ(memcmp(b.data(), "0123456789", 10), 0);
}
TEST(BufferTest, SizeDecrease) {
Buffer b(8);
EXPECT_EQ(b.size(), 8);
EXPECT_EQ(b.capacity(), 8);
EXPECT_NE(b.data(), nullptr);
memcpy(b.data(), "01234567", 8);
// Should not realloc the buffer and keep data.
b.resize(2);
EXPECT_EQ(b.size(), 2);
EXPECT_EQ(b.capacity(), 8);
EXPECT_NE(b.data(), nullptr);
EXPECT_EQ(memcmp(b.data(), "01", 2), 0);
}
TEST(BufferTest, Reserve) {
Buffer b({1});
Buffer b2({1});
Buffer b3({2});
EXPECT_TRUE(b == b2);
EXPECT_FALSE(b == b3);
EXPECT_FALSE(b != b2);
EXPECT_TRUE(b != b3);
}
TEST(BufferTest, Empty) {
Buffer b;
EXPECT_TRUE(b.empty());
b = {};
EXPECT_TRUE(b.empty());
b = {1};
EXPECT_FALSE(b.empty());
}
TEST(BufferTest, Append) {
Buffer b;
b.append("9", 1);
EXPECT_EQ(b, Buffer({'9'}));
b.append("123", 3);
EXPECT_EQ(b, Buffer({'9', '1', '2', '3'}));
}
TEST(BufferTest, Clear) {
Buffer b(8);
EXPECT_EQ(b.size(), 8);
EXPECT_EQ(b.capacity(), 8);
EXPECT_NE(b.data(), nullptr);
b.clear();
EXPECT_EQ(b.size(), 0);
EXPECT_EQ(b.capacity(), 8);
EXPECT_NE(b.data(), nullptr);
}
} // namespace
} // namespace cdc_ft

64
common/clock.cc Normal file
View File

@@ -0,0 +1,64 @@
// 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 "common/clock.h"
#include "absl/strings/str_format.h"
#include "common/platform.h"
namespace cdc_ft {
// static
DefaultSteadyClock* DefaultSteadyClock::GetInstance() {
static DefaultSteadyClock instance;
return &instance;
}
SteadyClock::Timestamp DefaultSteadyClock::Now() const {
return std::chrono::steady_clock::now();
}
std::string SystemClock::FormatNow(const char* format,
bool append_millis) const {
const Timestamp now = Now();
const std::time_t now_t = std::chrono::system_clock::to_time_t(now);
tm now_tm;
#if PLATFORM_WINDOWS
localtime_s(&now_tm, &now_t);
#elif PLATFORM_LINUX
localtime_r(&now_t, &now_tm);
#endif
std::stringstream ss;
ss << std::put_time(&now_tm, format);
if (append_millis) {
const int millis = std::chrono::duration_cast<std::chrono::milliseconds>(
now - std::chrono::system_clock::from_time_t(now_t))
.count();
ss << std::setfill('0') << std::setw(3) << millis;
}
return ss.str();
}
// static
DefaultSystemClock* DefaultSystemClock::GetInstance() {
static DefaultSystemClock instance;
return &instance;
}
SystemClock::Timestamp DefaultSystemClock::Now() const {
return std::chrono::system_clock::now();
}
} // namespace cdc_ft

61
common/clock.h Normal file
View File

@@ -0,0 +1,61 @@
/*
* 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 COMMON_CLOCK_H_
#define COMMON_CLOCK_H_
#include <chrono>
#include <string>
namespace cdc_ft {
// Clock that never runs backwards. Useful for stopwatch etc.
class SteadyClock {
public:
using Timestamp = std::chrono::time_point<std::chrono::steady_clock>;
virtual ~SteadyClock() = default;
virtual Timestamp Now() const = 0;
};
class DefaultSteadyClock : public SteadyClock {
public:
static DefaultSteadyClock* GetInstance();
Timestamp Now() const override;
};
// Clock that matches the system's clock.
class SystemClock {
public:
using Timestamp = std::chrono::time_point<std::chrono::system_clock>;
virtual ~SystemClock() = default;
virtual Timestamp Now() const = 0;
// Formats the current timestamp. |format| is the format according to the
// std::put_time specification, see
// https://en.cppreference.com/w/cpp/io/manip/put_time.
// If |append_millis| is true, appends the milliseconds formatted as %03i.
std::string FormatNow(const char* format, bool append_millis) const;
};
class DefaultSystemClock : public SystemClock {
public:
static DefaultSystemClock* GetInstance();
Timestamp Now() const override;
};
} // namespace cdc_ft
#endif // COMMON_CLOCK_H_

287
common/dir_iter.cc Normal file
View File

@@ -0,0 +1,287 @@
// 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 "common/dir_iter.h"
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <cassert>
#include <list>
#if defined(_MSC_VER)
#include "dirent.h"
#else
#include <dirent.h>
#endif
#include <algorithm>
#include <memory>
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "common/errno_mapping.h"
#include "common/path.h"
namespace cdc_ft {
namespace {
const std::string empty_string;
}
// Allows testing if a specific flag is set using the & operator.
inline int operator&(DirectorySearchFlags a, DirectorySearchFlags b) {
using T = std::underlying_type_t<DirectorySearchFlags>;
return static_cast<int>(static_cast<T>(a) & static_cast<T>(b));
}
class DirectoryEntry::Impl {
friend class DirectoryEntry;
public:
Impl(const std::string& rel_path, const dirent* dent)
: rel_path(rel_path),
name(dent ? dent->d_name : ""),
type(dent ? dent->d_type : 0) {}
const std::string rel_path;
const std::string name;
const int type;
};
DirectoryEntry::DirectoryEntry() : impl_(nullptr) {}
DirectoryEntry::~DirectoryEntry() {
if (impl_) delete impl_;
}
bool DirectoryEntry::Valid() const { return impl_ && impl_->type; }
bool DirectoryEntry::IsDir() const { return impl_ && impl_->type & DT_DIR; }
bool DirectoryEntry::IsRegularFile() const {
return impl_ && impl_->type & DT_REG;
}
bool DirectoryEntry::IsSymlink() const { return impl_ && impl_->type & DT_LNK; }
const std::string& DirectoryEntry::Name() const {
return impl_ ? impl_->name : empty_string;
}
const std::string& DirectoryEntry::RelPath() const {
return impl_ ? impl_->rel_path : empty_string;
}
std::string DirectoryEntry::RelPathName() const {
return impl_ ? path::Join(impl_->rel_path, impl_->name) : std::string();
}
void DirectoryEntry::Clear() { SetImpl(nullptr); }
void DirectoryEntry::SetImpl(Impl* impl) {
if (impl_) delete impl_;
impl_ = impl;
}
class DirectoryIterator::Impl {
public:
Impl(const std::string& path, DirectorySearchFlags results, bool recursive)
: path_(path),
flags_(results),
recursive_(recursive),
last_dir_(nullptr) {}
~Impl() {
while (!DirsEmpty()) PopDir();
}
inline const std::string& Path() const { return path_; }
inline bool ReturnDirs() const {
return flags_ & DirectorySearchFlags::kDirectories;
}
inline bool ReturnFiles() const {
return flags_ & DirectorySearchFlags::kFiles;
}
inline bool Recursive() const { return recursive_; }
// Reads the next directory entry from the topmost opened directory. Returns
// nullptr when all directory entries have been read or in case of an error.
// Check errno to distinguish between those two cases.
inline struct dirent* NextDirEntry() {
assert(!dirs_.empty());
// Update relative path if it changed.
DIR* dir = dirs_.back().dir;
if (last_dir_ != dir) {
UpdateRelPath();
last_dir_ = dir;
}
// Reset previous error so that we know if we reached the end the dir.
errno = 0;
return readdir(dir);
}
inline const std::string& RelPath() const { return rel_path_; }
inline std::string DirsPath() { return path::Join(path_, rel_path_); }
inline void PushDir(DIR* dir, const std::string& name) {
assert(dir != nullptr);
dirs_.push_back({dir, name});
}
inline void PopDir() {
assert(!dirs_.empty());
closedir(dirs_.back().dir);
dirs_.pop_back();
}
inline bool DirsEmpty() const { return dirs_.empty(); }
inline absl::Status Status() const { return status_; }
inline void SetStatus(absl::Status status) { status_ = status; }
private:
// On Windows, the struct DIR::ent doesn't seem properly aligned, so the
// DIR::ent::d_name field is unusable. Thus, we store the DIR pointer along
// with its name in this struct.
struct OpenedDir {
DIR* dir;
std::string name;
};
inline void UpdateRelPath() {
rel_path_.clear();
bool first = true;
for (const OpenedDir& dir : dirs_) {
if (first) {
// The first component is already included in path_.
first = false;
continue;
}
path::Append(&rel_path_, dir.name);
}
}
const std::string path_;
const DirectorySearchFlags flags_;
const bool recursive_;
absl::Status status_;
std::list<OpenedDir> dirs_;
const DIR* last_dir_;
std::string rel_path_;
};
DirectoryIterator::DirectoryIterator() : impl_(nullptr) {}
DirectoryIterator::DirectoryIterator(const std::string& path,
DirectorySearchFlags results,
bool recursive)
: impl_(nullptr) {
Open(path, results, recursive);
}
DirectoryIterator::~DirectoryIterator() {
if (impl_) delete impl_;
}
absl::Status DirectoryIterator::Status() const {
return impl_ ? impl_->Status() : absl::OkStatus();
}
bool DirectoryIterator::Valid() const {
return impl_ && impl_->Status().ok() && !impl_->DirsEmpty();
}
const std::string& DirectoryIterator::Path() const {
return impl_ ? impl_->Path() : empty_string;
}
bool DirectoryIterator::Open(const std::string& path,
DirectorySearchFlags results, bool recursive) {
if (impl_) delete impl_;
impl_ = new Impl(path, results, recursive);
DIR* dir = opendir(path.c_str());
if (dir) {
impl_->PushDir(dir, path::BaseName(path));
return true;
}
impl_->SetStatus(ErrnoToCanonicalStatus(
errno, absl::StrFormat("Failed to open directory '%s'", path)));
return false;
}
bool DirectoryIterator::NextEntry(DirectoryEntry* entry) {
if (!impl_ || !impl_->Status().ok()) return false;
// Reset last error to not report a previous one below.
errno = 0;
while (!impl_->DirsEmpty()) {
struct dirent* dent = impl_->NextDirEntry();
if (!dent) {
if (!errno) {
// We have reached the end of this directory.
impl_->PopDir();
continue;
}
break;
}
// Handle directories.
if (dent->d_type & DT_DIR) {
// Skip "." and ".." directory entries.
if (!strcmp(dent->d_name, ".") || !strcmp(dent->d_name, "..")) {
continue;
}
// For recursive traversal, push new directory on top of the stack.
if (impl_->Recursive()) {
std::string dname(dent->d_name);
std::string subdir = path::Join(impl_->DirsPath(), dname);
DIR* dir = opendir(subdir.c_str());
if (dir) {
impl_->PushDir(dir, dname);
} else if (errno == EACCES) {
// Ignore access errors and proceed.
} else {
impl_->SetStatus(ErrnoToCanonicalStatus(
errno, absl::StrFormat("Failed to open directory '%s'", subdir)));
return false;
}
}
if (impl_->ReturnDirs()) {
entry->SetImpl(new DirectoryEntry::Impl(impl_->RelPath(), dent));
return true;
}
} else if (dent->d_type & DT_REG) {
// Handle regular files.
if (impl_->ReturnFiles()) {
entry->SetImpl(new DirectoryEntry::Impl(impl_->RelPath(), dent));
return true;
}
}
}
if (errno) {
impl_->SetStatus(ErrnoToCanonicalStatus(
errno, absl::StrFormat("Failed to iterate over directory '%s'",
impl_->DirsPath())));
}
return false;
}
}; // namespace cdc_ft

145
common/dir_iter.h Normal file
View File

@@ -0,0 +1,145 @@
/*
* 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 COMMON_DIR_ITER_H_
#define COMMON_DIR_ITER_H_
#include <cstdio>
#include <string>
#include "absl/status/status.h"
#include "absl/strings/string_view.h"
namespace cdc_ft {
// Flags for selecting the type of directory entries when iterating. This allows
// one to narrow down the results that a DirectoryIterator returns.
enum class DirectorySearchFlags {
kNone = 0,
kDirectories = (1 << 0),
kFiles = (1 << 1),
kFilesAndDirectories = kFiles | kDirectories,
};
class DirectoryIterator;
// A DirectoryEntry describes a file or a directory that was found when
// iterating through the file system.
class DirectoryEntry {
public:
DirectoryEntry();
~DirectoryEntry();
// Copy and assignment are disabled due to the separate implementation class.
DirectoryEntry(DirectoryEntry const&) = delete;
DirectoryEntry& operator=(DirectoryEntry const&) = delete;
// Returns true if the entry describes an existing item.
bool Valid() const;
// Returns true if the entry is valid and describes a directory.
bool IsDir() const;
// Returns true if the entry is valid and describes a regular file.
bool IsRegularFile() const;
// Returns true if the entry is valid and describes a symlink.
bool IsSymlink() const;
// Returns the name for this entry, without any path component.
const std::string& Name() const;
// Returns the relative path from the originating DirectoryIterator to the
// directory that contains this entry (excluding this entry).
const std::string& RelPath() const;
// Returns the relative path and filename from the originating
// DirectoryIterator to the this entry (including this entry).
std::string RelPathName() const;
// Frees all data associated with this entry. After calling Clear(), this
// entry is no longer valid.
void Clear();
private:
class Impl;
friend class DirectoryIterator;
void SetImpl(Impl* impl);
Impl* impl_;
};
// This class allows recursive listing of directory contents.
class DirectoryIterator {
public:
// Default constructor.
DirectoryIterator();
// Contructs a new iterator and immediately calls Open() using the given
// parameters.
DirectoryIterator(
const std::string& path,
DirectorySearchFlags results = DirectorySearchFlags::kFilesAndDirectories,
bool recursive = true);
// Destructor
~DirectoryIterator();
// Copy and assignment are disabled due to the separate implementation class.
DirectoryIterator(DirectoryIterator const&) = delete;
DirectoryIterator& operator=(DirectoryIterator const&) = delete;
// Returns any error that might have occured so far. Note that the iterator
// ignores any permission errors that might occur when recursing into
// restricted sub-directories and just continues with the next directory.
absl::Status Status() const;
// Returns true as long as a directory has been opened, no error has occured,
// and a call to NextEntry() has a chance to succeed.
bool Valid() const;
// Returns the path that was given to open the first directory.
const std::string& Path() const;
// Opens the given directory path for reading. If this method returns true, a
// directory entry may be fetched by calling NextEntry(). The results
// parameter can be used to control which types of directory entries a call to
// NextEntry() might yield. The recustive parameter controls whether or not
// the iterator recurses into sub-directories in a DFS manner.
//
// In case of an error (including permission errors), this function returns
// false. Check Status() for the actual error in that case.
bool Open(
const std::string& path,
DirectorySearchFlags results = DirectorySearchFlags::kFilesAndDirectories,
bool recursive = true);
// Yields the next directory entry that matches the DirectorySearchFlags that
// were given in the call to Open(). Returns true if a new entry was found,
// false in case of an error or if no more entries are available. Check
// Status() to distinguish between those two cases.
//
// Note: The iterator ignores permission errors that occur in any
// sub-directory and just continues with the next directory.
bool NextEntry(DirectoryEntry* entry);
private:
class Impl;
Impl* impl_;
};
}; // namespace cdc_ft
#endif // COMMON_DIR_ITER_H_

267
common/dir_iter_test.cc Normal file
View File

@@ -0,0 +1,267 @@
// 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 "common/dir_iter.h"
#include <algorithm>
#include <iterator>
#include "common/path.h"
#include "common/platform.h"
#include "common/status_test_macros.h"
#include "common/test_main.h"
#include "gtest/gtest.h"
namespace cdc_ft {
namespace {
class DirectoryIteratorTest : public ::testing::Test {
public:
using StringSet = std::set<std::string>;
DirectoryIteratorTest() {}
void SetUp() override {
expected_files_.insert("a/aa/aaa1.txt");
expected_files_.insert("a/aa/aaa2.txt");
expected_files_.insert("a/aa1.txt");
expected_files_.insert("a/aa2.txt");
expected_files_.insert("a/ab/aab1.txt");
expected_files_.insert("a/ab/aab2.txt");
expected_files_.insert("b/ba/bba1.txt");
expected_files_.insert("b/ba/bba2.txt");
expected_files_.insert("b/bb/bbb1.txt");
expected_files_.insert("b/bb/bbb2.txt");
expected_files_.insert("c/c1.txt");
expected_files_.insert("c/c2.txt");
expected_files_.insert("d/d1.txt");
expected_files_.insert("d/d2.txt");
expected_files_.insert("root.txt");
expected_dirs_.insert("a");
expected_dirs_.insert("a/aa");
expected_dirs_.insert("a/ab");
expected_dirs_.insert("b");
expected_dirs_.insert("b/ba");
expected_dirs_.insert("b/bb");
expected_dirs_.insert("c");
expected_dirs_.insert("d");
}
static StringSet Union(const StringSet& a, const StringSet& b) {
StringSet out(a);
out.insert(b.begin(), b.end());
return out;
}
static StringSet Filter(
const StringSet& items,
std::function<bool(const std::string& item)> filter_fn) {
StringSet out;
std::copy_if(items.begin(), items.end(), std::inserter(out, out.begin()),
[&](const std::string& f) { return !filter_fn(f); });
return out;
}
protected:
std::set<std::string> expected_files_;
std::set<std::string> expected_dirs_;
std::string test_data_dir_ = GetTestDataDir("dir_iter");
};
TEST_F(DirectoryIteratorTest, EmptyDirIterator) {
DirectoryIterator dit;
DirectoryEntry dent;
EXPECT_OK(dit.Status());
EXPECT_FALSE(dit.Valid());
EXPECT_EQ(dit.Path(), std::string());
EXPECT_FALSE(dit.NextEntry(&dent));
}
TEST_F(DirectoryIteratorTest, ValidDirIterator) {
DirectoryIterator dit(test_data_dir_);
DirectoryEntry dent;
EXPECT_OK(dit.Status());
EXPECT_TRUE(dit.Valid());
EXPECT_EQ(dit.Path(), test_data_dir_);
EXPECT_TRUE(dit.NextEntry(&dent));
}
TEST_F(DirectoryIteratorTest, FindFiles) {
DirectoryIterator dit(test_data_dir_, DirectorySearchFlags::kFiles, false);
DirectoryEntry dent;
std::set<std::string> found;
while (dit.NextEntry(&dent)) {
found.insert(path::ToUnix(dent.RelPathName()));
}
EXPECT_OK(dit.Status());
std::set<std::string> expected = Filter(
expected_files_,
[](const std::string& f) { return f.find('/') != std::string::npos; });
EXPECT_EQ(found, expected);
}
TEST_F(DirectoryIteratorTest, FindDirectories) {
DirectoryIterator dit(test_data_dir_, DirectorySearchFlags::kDirectories,
false);
DirectoryEntry dent;
std::set<std::string> found;
while (dit.NextEntry(&dent)) {
found.insert(path::ToUnix(dent.RelPathName()));
}
EXPECT_OK(dit.Status());
std::set<std::string> expected = Filter(
expected_dirs_,
[](const std::string& f) { return f.find('/') != std::string::npos; });
EXPECT_EQ(found, expected);
}
TEST_F(DirectoryIteratorTest, FindFilesAndDirectories) {
DirectoryIterator dit(test_data_dir_,
DirectorySearchFlags::kFilesAndDirectories, false);
DirectoryEntry dent;
std::set<std::string> found;
while (dit.NextEntry(&dent)) {
found.insert(path::ToUnix(dent.RelPathName()));
}
EXPECT_OK(dit.Status());
std::set<std::string> expected = Filter(
Union(expected_dirs_, expected_files_),
[](const std::string& f) { return f.find('/') != std::string::npos; });
EXPECT_EQ(found, expected);
}
TEST_F(DirectoryIteratorTest, FindFilesRecursive) {
DirectoryIterator dit(test_data_dir_, DirectorySearchFlags::kFiles);
DirectoryEntry dent;
std::set<std::string> found;
while (dit.NextEntry(&dent)) {
found.insert(path::ToUnix(dent.RelPathName()));
}
EXPECT_OK(dit.Status());
EXPECT_EQ(found, expected_files_);
}
TEST_F(DirectoryIteratorTest, FindDirectoriesRecursive) {
DirectoryIterator dit(test_data_dir_, DirectorySearchFlags::kDirectories);
DirectoryEntry dent;
std::set<std::string> found;
while (dit.NextEntry(&dent)) {
found.insert(path::ToUnix(dent.RelPathName()));
}
EXPECT_OK(dit.Status());
EXPECT_EQ(found, expected_dirs_);
}
TEST_F(DirectoryIteratorTest, FindFilesAndDirectoriesRecursive) {
DirectoryIterator dit(test_data_dir_);
DirectoryEntry dent;
std::set<std::string> found;
while (dit.NextEntry(&dent)) {
found.insert(path::ToUnix(dent.RelPathName()));
}
EXPECT_OK(dit.Status());
EXPECT_EQ(found, Union(expected_dirs_, expected_files_));
}
TEST_F(DirectoryIteratorTest, DirNotFound) {
DirectoryIterator dit("does/not/exist");
DirectoryEntry dent;
std::set<std::string> found;
while (dit.NextEntry(&dent)) {
found.insert(path::ToUnix(dent.RelPathName()));
}
EXPECT_TRUE(found.empty());
EXPECT_TRUE(absl::IsNotFound(dit.Status()));
}
TEST_F(DirectoryIteratorTest, HandleError) {
// Trying to open a file instead of a directory.
DirectoryIterator dit(path::Join(test_data_dir_, "root.txt"));
DirectoryEntry dent;
std::set<std::string> found;
while (dit.NextEntry(&dent)) {
found.insert(path::ToUnix(dent.RelPathName()));
}
EXPECT_TRUE(found.empty());
EXPECT_NOT_OK(dit.Status());
}
TEST_F(DirectoryIteratorTest, EmptyDirEntry) {
DirectoryEntry dent;
EXPECT_FALSE(dent.Valid());
EXPECT_FALSE(dent.IsDir());
EXPECT_FALSE(dent.IsRegularFile());
EXPECT_FALSE(dent.IsSymlink());
EXPECT_EQ(dent.Name(), std::string());
EXPECT_EQ(dent.RelPath(), std::string());
EXPECT_EQ(dent.RelPathName(), std::string());
}
TEST_F(DirectoryIteratorTest, DirEntryForFile) {
DirectoryIterator dit(test_data_dir_, DirectorySearchFlags::kFiles, false);
DirectoryEntry dent;
EXPECT_TRUE(dit.NextEntry(&dent));
EXPECT_OK(dit.Status());
EXPECT_TRUE(dent.Valid());
EXPECT_FALSE(dent.IsDir());
EXPECT_TRUE(dent.IsRegularFile());
#ifdef PLATFORM_WINDOWS
// On Windows, Bazel does not copy nor link data dependencies.
EXPECT_FALSE(dent.IsSymlink());
#else
// On Linux, Bazel creates symlinks for files in data dependencies.
EXPECT_TRUE(dent.IsSymlink());
#endif
EXPECT_EQ(dent.Name(), "root.txt");
EXPECT_EQ(dent.RelPath(), std::string());
EXPECT_EQ(dent.RelPathName(), "root.txt");
}
TEST_F(DirectoryIteratorTest, DirEntryForDir) {
std::set<std::string> expected_any = Filter(
expected_dirs_,
[](const std::string& f) { return f.find('/') != std::string::npos; });
DirectoryIterator dit(test_data_dir_, DirectorySearchFlags::kDirectories,
false);
DirectoryEntry dent;
EXPECT_TRUE(dit.NextEntry(&dent));
EXPECT_OK(dit.Status());
EXPECT_TRUE(dent.Valid());
EXPECT_TRUE(dent.IsDir());
EXPECT_FALSE(dent.IsRegularFile());
EXPECT_FALSE(dent.IsSymlink());
EXPECT_TRUE(expected_any.find(dent.Name()) != expected_any.end());
EXPECT_EQ(dent.RelPath(), std::string());
EXPECT_TRUE(expected_any.find(dent.RelPathName()) != expected_any.end());
}
TEST_F(DirectoryIteratorTest, DirEntryClear) {
DirectoryIterator dit(test_data_dir_, DirectorySearchFlags::kFiles, false);
DirectoryEntry dent;
EXPECT_TRUE(dit.NextEntry(&dent));
EXPECT_OK(dit.Status());
dent.Clear();
EXPECT_FALSE(dent.Valid());
EXPECT_FALSE(dent.IsDir());
EXPECT_FALSE(dent.IsRegularFile());
EXPECT_FALSE(dent.IsSymlink());
EXPECT_EQ(dent.Name(), std::string());
EXPECT_EQ(dent.RelPath(), std::string());
EXPECT_EQ(dent.RelPathName(), std::string());
}
} // namespace
} // namespace cdc_ft

169
common/errno_mapping.cc Normal file
View File

@@ -0,0 +1,169 @@
// 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 "common/errno_mapping.h"
#include <errno.h>
#include <string>
namespace cdc_ft {
absl::StatusCode ErrnoToCanonicalCode(int error_number) {
switch (error_number) {
case 0:
return absl::StatusCode::kOk;
case EINVAL: // Invalid argument
case ENAMETOOLONG: // Filename too long
case E2BIG: // Argument list too long
case EDESTADDRREQ: // Destination address required
case EDOM: // Mathematics argument out of domain of function
case EFAULT: // Bad address
case EILSEQ: // Illegal byte sequence
case ENOPROTOOPT: // Protocol not available
case ENOSTR: // Not a STREAM
case ENOTSOCK: // Not a socket
case ENOTTY: // Inappropriate I/O control operation
case EPROTOTYPE: // Protocol wrong type for socket
case ESPIPE: // Invalid seek
return absl::StatusCode::kInvalidArgument;
case ETIMEDOUT: // Connection timed out
case ETIME: // Timer expired
return absl::StatusCode::kDeadlineExceeded;
case ENODEV: // No such device
case ENOENT: // No such file or directory
#ifdef ENOMEDIUM
case ENOMEDIUM: // No medium found
#endif
case ENXIO: // No such device or address
case ESRCH: // No such process
return absl::StatusCode::kNotFound;
case EEXIST: // File exists
case EADDRNOTAVAIL: // Address not available
case EALREADY: // Connection already in progress
#ifdef ENOTUNIQ
case ENOTUNIQ: // Name not unique on network
#endif
return absl::StatusCode::kAlreadyExists;
case EPERM: // Operation not permitted
case EACCES: // Permission denied
#ifdef ENOKEY
case ENOKEY: // Required key not available
#endif
case EROFS: // Read only file system
return absl::StatusCode::kPermissionDenied;
case ENOTEMPTY: // Directory not empty
case EISDIR: // Is a directory
case ENOTDIR: // Not a directory
case EADDRINUSE: // Address already in use
case EBADF: // Invalid file descriptor
#ifdef EBADFD
case EBADFD: // File descriptor in bad state
#endif
case EBUSY: // Device or resource busy
case ECHILD: // No child processes
case EISCONN: // Socket is connected
#ifdef EISNAM
case EISNAM: // Is a named type file
#endif
#ifdef ENOTBLK
case ENOTBLK: // Block device required
#endif
case ENOTCONN: // The socket is not connected
case EPIPE: // Broken pipe
#ifdef ESHUTDOWN
case ESHUTDOWN: // Cannot send after transport endpoint shutdown
#endif
case ETXTBSY: // Text file busy
#ifdef EUNATCH
case EUNATCH: // Protocol driver not attached
#endif
return absl::StatusCode::kFailedPrecondition;
case ENOSPC: // No space left on device
#ifdef EDQUOT
case EDQUOT: // Disk quota exceeded
#endif
case EMFILE: // Too many open files
case EMLINK: // Too many links
case ENFILE: // Too many open files in system
case ENOBUFS: // No buffer space available
case ENODATA: // No message is available on the STREAM read queue
case ENOMEM: // Not enough space
case ENOSR: // No STREAM resources
#ifdef EUSERS
case EUSERS: // Too many users
#endif
return absl::StatusCode::kResourceExhausted;
#ifdef ECHRNG
case ECHRNG: // Channel number out of range
#endif
case EFBIG: // File too large
case EOVERFLOW: // Value too large to be stored in data type
case ERANGE: // Result too large
return absl::StatusCode::kOutOfRange;
#ifdef ENOPKG
case ENOPKG: // Package not installed
#endif
case ENOSYS: // Function not implemented
case ENOTSUP: // Operation not supported
case EAFNOSUPPORT: // Address family not supported
#ifdef EPFNOSUPPORT
case EPFNOSUPPORT: // Protocol family not supported
#endif
case EPROTONOSUPPORT: // Protocol not supported
#ifdef ESOCKTNOSUPPORT
case ESOCKTNOSUPPORT: // Socket type not supported
#endif
case EXDEV: // Improper link
return absl::StatusCode::kUnimplemented;
case EAGAIN: // Resource temporarily unavailable
#ifdef ECOMM
case ECOMM: // Communication error on send
#endif
case ECONNREFUSED: // Connection refused
case ECONNABORTED: // Connection aborted
case ECONNRESET: // Connection reset
case EINTR: // Interrupted function call
#ifdef EHOSTDOWN
case EHOSTDOWN: // Host is down
#endif
case EHOSTUNREACH: // Host is unreachable
case ENETDOWN: // Network is down
case ENETRESET: // Connection aborted by network
case ENETUNREACH: // Network unreachable
case ENOLCK: // No locks available
case ENOLINK: // Link has been severed
#ifdef ENONET
case ENONET: // Machine is not on the network
#endif
return absl::StatusCode::kUnavailable;
case EDEADLK: // Resource deadlock avoided
#ifdef ESTALE
case ESTALE: // Stale file handle
#endif
return absl::StatusCode::kAborted;
case ECANCELED: // Operation cancelled
return absl::StatusCode::kCancelled;
default:
return absl::StatusCode::kUnknown;
}
}
absl::Status ErrnoToCanonicalStatus(int error_number,
absl::string_view message) {
return absl::Status(ErrnoToCanonicalCode(error_number),
absl::StrCat(message, ": ", strerror(error_number)));
}
} // namespace cdc_ft

34
common/errno_mapping.h Normal file
View File

@@ -0,0 +1,34 @@
/*
* 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 COMMON_ERRNO_MAPPING_H_
#define COMMON_ERRNO_MAPPING_H_
#include "absl/status/status.h"
#include "absl/strings/string_view.h"
namespace cdc_ft {
// Converts the errno |error_number| to an absl code.
absl::StatusCode ErrnoToCanonicalCode(int error_number);
// Creates a status by converting the errno |error_number| to an absl code.
absl::Status ErrnoToCanonicalStatus(int error_number,
absl::string_view message);
} // namespace cdc_ft
#endif // COMMON_ERRNO_MAPPING_H_

View File

@@ -0,0 +1,54 @@
// 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 "common/errno_mapping.h"
#include <errno.h>
#include <stddef.h>
#include <string>
#include "common/status_test_macros.h"
#include "gtest/gtest.h"
namespace cdc_ft {
namespace {
// Errno value that is hopefully not defined on any OS.
const int kUndefinedErrno = 999123;
TEST(ErrnoMappingTest, ErrnoToCanonicalCode) {
EXPECT_EQ(ErrnoToCanonicalCode(0), absl::StatusCode::kOk);
// Spot-check a few errno values.
EXPECT_EQ(ErrnoToCanonicalCode(EINVAL), absl::StatusCode::kInvalidArgument);
EXPECT_EQ(ErrnoToCanonicalCode(ENOENT), absl::StatusCode::kNotFound);
EXPECT_EQ(ErrnoToCanonicalCode(kUndefinedErrno), absl::StatusCode::kUnknown);
}
TEST(ErrnoMappingTest, ErrnoToCanonicalStatus) {
EXPECT_OK(ErrnoToCanonicalStatus(0, ""));
// Spot-check a few errno values.
EXPECT_ERROR_MSG(InvalidArgument, "test0",
ErrnoToCanonicalStatus(EINVAL, "test0"));
EXPECT_ERROR_MSG(NotFound, "test1", ErrnoToCanonicalStatus(ENOENT, "test1"));
// Apparently errno 999 is known not to be defined.
EXPECT_ERROR_MSG(Unknown, "test2",
ErrnoToCanonicalStatus(kUndefinedErrno, "test2"));
}
} // namespace
} // namespace cdc_ft

629
common/file_watcher_win.cc Normal file
View File

@@ -0,0 +1,629 @@
// 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 "common/file_watcher_win.h"
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <atomic>
#include <thread>
#include "absl/status/statusor.h"
#include "absl/strings/str_format.h"
#include "common/log.h"
#include "common/path.h"
#include "common/scoped_handle_win.h"
#include "common/status.h"
#include "common/status_macros.h"
#include "common/stopwatch.h"
#include "common/util.h"
namespace cdc_ft {
namespace {
static constexpr DWORD kFileNotificationFlags =
FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME |
FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE;
static constexpr size_t kDefaultBufferSize = 1U << 18; // 256 KiB.
void CancelDirIo(ScopedHandle& dir_handle, const std::wstring& dir_path) {
if (dir_handle.IsValid() && !CancelIo(dir_handle.Get())) {
LOG_ERROR("CancelIo() failed for directory '%s': '%s'",
Util::WideToUtf8Str(dir_path), Util::GetLastWin32Error());
}
dir_handle.Close();
}
int64_t ToUnixTime(LARGE_INTEGER windows_time) {
// Jan 1, 1970 (begin of Unix epoch) in 100 ns ticks since Jan 1, 1601 (begin
// of Windows epoch).
constexpr int64_t kUnixEpochOffset = 0x019DB1DED53E8000;
// A Windows tick is 100 ns.
constexpr int64_t kWinTicksPerSecond = 10000000;
return (windows_time.QuadPart - kUnixEpochOffset) / kWinTicksPerSecond;
}
} // namespace
// Background thread to read directory changes.
class AsyncFileWatcher {
public:
enum class FileWatcherState { kDefault, kFailed, kRunning, kShuttingDown };
using FileAction = FileWatcherWin::FileAction;
using FileInfo = FileWatcherWin::FileInfo;
using FileMap = FileWatcherWin::FileMap;
using FilesChangedCb = FileWatcherWin::FilesChangedCb;
using DirRecreatedCb = FileWatcherWin::DirRecreatedCb;
AsyncFileWatcher(std::string dir_path, FilesChangedCb files_changed_cb,
DirRecreatedCb dir_recreated_cb, unsigned int timeout_ms,
bool enforceLegacyReadDirectoryChangesForTesting)
: dir_path_(dir_path),
files_changed_cb_(std::move(files_changed_cb)),
dir_recreated_cb_(std::move(dir_recreated_cb)),
timeout_ms_(timeout_ms) {
// (internal): Check whether ReadDirectoryChangesExW is available.
// It requires Windows 10, version 1709, released October 17, 2017, or
// corresponding Windows Server versions.
if (!enforceLegacyReadDirectoryChangesForTesting) {
read_directory_changes_ex_ =
reinterpret_cast<decltype(ReadDirectoryChangesExW)*>(::GetProcAddress(
::GetModuleHandle(L"Kernel32.dll"), "ReadDirectoryChangesExW"));
}
shutdown_event_ = ScopedHandle(CreateEvent(nullptr, TRUE, FALSE, nullptr));
if (!shutdown_event_.IsValid()) {
SetStatus(absl::InternalError(absl::StrFormat(
"Failed to create shutdown event: '%s'", Util::GetLastWin32Error())));
return;
}
dir_reader_ = std::thread([this]() { WatchDirChanges(); });
}
~AsyncFileWatcher() { Shutdown(); }
absl::Status GetStatus() ABSL_LOCKS_EXCLUDED(status_mutex_) const {
absl::MutexLock mutex(&status_mutex_);
return status_;
}
FileMap GetModifiedFiles() ABSL_LOCKS_EXCLUDED(modified_files_mutex_) {
FileMap files;
absl::MutexLock mutex(&modified_files_mutex_);
std::swap(modified_files_, files);
// Retrieve file stats if ReadDirectoryChangesEx is not available.
if (!read_directory_changes_ex_ && !files.empty()) {
Stopwatch sw;
for (auto& [path, info] : files) {
if (info.action == FileAction::kDeleted) continue;
std::string full_path = path::Join(dir_path_, path);
path::Stats stats;
absl::Status status = path::GetStats(full_path, &stats);
if (!status.ok()) {
LOG_WARNING("Failed to get stats for path '%s'", full_path);
continue;
}
// Don't use the Windows localized timestamp from GetStats.
time_t mtime;
status = path::GetFileTime(full_path, &mtime);
if (!status.ok()) {
LOG_WARNING("Failed to get modification time for path '%s'",
full_path);
continue;
}
info.is_dir = (stats.mode & path::MODE_IFDIR) != 0;
info.size = stats.size;
info.mtime = mtime;
}
LOG_DEBUG("Time to fix file stats: %0.3f sec.", sw.ElapsedSeconds());
}
return files;
}
void ClearModifiedFiles() ABSL_LOCKS_EXCLUDED(modified_files_mutex_) {
absl::MutexLock mutex(&modified_files_mutex_);
modified_files_.clear();
}
uint32_t GetEventCount() ABSL_LOCKS_EXCLUDED(modified_files_mutex_) const {
absl::MutexLock mutex(&modified_files_mutex_);
return event_count_;
}
uint32_t GetDirRecreateEventCount()
ABSL_LOCKS_EXCLUDED(modified_files_mutex_) const {
absl::MutexLock mutex(&modified_files_mutex_);
return dir_recreate_count_;
}
bool IsWatching() ABSL_LOCKS_EXCLUDED(state_mutex) const {
absl::MutexLock mutex(&state_mutex_);
return state_ != FileWatcherState::kDefault &&
state_ != FileWatcherState::kShuttingDown;
}
bool IsShuttingDown() ABSL_LOCKS_EXCLUDED(state_mutex) const {
absl::MutexLock mutex(&state_mutex_);
return state_ == FileWatcherState::kShuttingDown;
}
void Shutdown() {
if (shutdown_event_.IsValid() && !SetEvent(shutdown_event_.Get())) {
LOG_ERROR("SetEvent() for shutdown failed: '%s'",
Util::GetLastWin32Error());
exit(1);
}
if (dir_reader_.joinable()) {
dir_reader_.join();
}
}
private:
// The most important method, which acquires directory handle and reads
// directory changes. It detects removal, creation, and re-creation of the
// watched directory. It is robust to the directory change on any level.
void WatchDirChanges() {
// TODO: Adjust also if there was no directory at the beginning. Currently,
// the directory exists; otherwise, ManifestUpdater would fail.
bool prev_dir_exists = true;
while (true) {
ScopedHandle read_event(CreateEvent(nullptr, /* no security attributes */
TRUE, /* manual-reset event */
FALSE, /* unsignaled */
nullptr)); /* unnamed event object */
if (!read_event.IsValid()) {
SetStatus(absl::InternalError(absl::StrFormat(
"Failed to create read event: '%s'", Util::GetLastWin32Error())));
return;
}
FILE_BASIC_INFO dir_info;
absl::StatusOr<ScopedHandle> status = GetValidDirHandle(&dir_info);
if (!status.ok()) {
SetStatus(status.status());
} else {
// The watched directory exists and its handle is valid.
if (!prev_dir_exists) {
++dir_recreate_count_;
prev_dir_exists = true;
SetStatus(absl::OkStatus());
if (dir_recreated_cb_) dir_recreated_cb_();
}
ReadDirChanges(*status, dir_info, read_event);
if (IsShuttingDown()) {
LOG_DEBUG("Shutting down watching '%s'.", dir_path_);
return;
}
LOG_WARNING("Watched directory '%s' was possibly removed.", dir_path_);
++dir_recreate_count_;
ClearModifiedFiles();
if (dir_recreated_cb_) dir_recreated_cb_();
}
prev_dir_exists = false;
// The shutdown event should be caught on both levels: when the
// watched directory was not removed and when it was recreated. Here
// the shutdown event is considered when the watched directory itself was
// removed or created. If a shutdown was triggered, stop watching. The
// current file-watcher status should not be changed.
if (IsShuttingDown() ||
WaitForSingleObject(shutdown_event_.Get(), timeout_ms_) ==
WAIT_OBJECT_0) {
LOG_DEBUG("Shutting down watching '%s'.", dir_path_);
ResetEvent(shutdown_event_.Get());
return;
}
}
}
// Creates a file handle for the watched directory and requests its
// properties. Returns a directory handle or an error if the directory does
// not exist or its properties could not be retrieved. |dir_info| is an output
// parameter, this is the basic information about the returned directory
// handle for the watched directory. It descibes for example creation and
// modification timestamps.
absl::StatusOr<ScopedHandle> GetValidDirHandle(
FILE_BASIC_INFO* dir_info) const {
assert(dir_info);
// Create handle for the watched directory.
ScopedHandle dir_handle(CreateFileW(
Util::Utf8ToWideStr(dir_path_).c_str(), /* watched directory */
FILE_LIST_DIRECTORY, /* rights to list the directory */
FILE_SHARE_READ | FILE_SHARE_WRITE |
FILE_SHARE_DELETE, /* sharing mode */
nullptr, /* security attributes */
OPEN_EXISTING, /* opens a file iff exists */
FILE_FLAG_BACKUP_SEMANTICS |
FILE_FLAG_OVERLAPPED, /* asynchronous I/O */
nullptr)); /* no template file */
// CreateFileW() failed and returned an invalid handle.
if (!dir_handle.IsValid()) {
return absl::FailedPreconditionError(
absl::StrFormat("Could not start watching '%s': '%s'", dir_path_,
Util::GetLastWin32Error()));
}
// Failed to retrieve basic information about the watched directory.
if (!GetFileInformationByHandleEx(dir_handle.Get(), FileBasicInfo, dir_info,
sizeof(*dir_info))) {
return absl::FailedPreconditionError(absl::StrFormat(
"Could not get information about directory '%s': '%s'", dir_path_,
Util::GetLastWin32Error()));
}
return dir_handle;
}
// Reads changes in the watched directory itself.
// In case of shutdown, the status is ok.
// In case of any error, an error status is set.
// It waits for changes in the watched directory, collects those events in
// |modified_files_|, and notifies the caller about those changes.
void ReadDirChanges(ScopedHandle& dir_handle, FILE_BASIC_INFO& dir_info,
ScopedHandle& read_event) {
OVERLAPPED overlapped;
ZeroMemory(&overlapped, sizeof(overlapped));
// Use FILE_NOTIFY_EXTENDED_INFORMATION if |read_directory_changes_ex_| is
// available and FILE_NOTIFY_INFORMATION otherwise.
constexpr size_t alignment =
std::max<size_t>(alignof(FILE_NOTIFY_EXTENDED_INFORMATION),
alignof(FILE_NOTIFY_INFORMATION));
std::aligned_storage_t<kDefaultBufferSize, alignment> buffer;
overlapped.hEvent = read_event.Get();
BOOL res;
if (read_directory_changes_ex_) {
res = read_directory_changes_ex_(
dir_handle.Get(), &buffer, sizeof(buffer),
true /* check subfolders */, kFileNotificationFlags,
nullptr /* not needed as async read */, &overlapped,
nullptr /* no completion routine */,
ReadDirectoryNotifyExtendedInformation);
} else {
res = ReadDirectoryChangesW(
dir_handle.Get(), &buffer, sizeof(buffer),
true /* check subfolders */, kFileNotificationFlags,
nullptr /* not needed as async read */, &overlapped,
nullptr /* no completion routine */);
}
if (res == FALSE && GetLastError() != ERROR_IO_PENDING) {
SetStatus(absl::InternalError(absl::StrFormat(
"Could not read changes in the watched directory '%s': '%s'",
dir_path_, Util::GetLastWin32Error())));
MaybeSetState(FileWatcherState::kFailed);
CancelDirIo(dir_handle, Util::Utf8ToWideStr(dir_path_));
return;
}
MaybeSetState(FileWatcherState::kRunning);
// Initialize handles to watch: changes in |dir_path_| and shutdown
// events.
HANDLE watch_handles[] = {overlapped.hEvent, shutdown_event_.Get()};
size_t read_index = 0;
size_t shutdown_index = 1;
while (true) {
std::aligned_storage_t<kDefaultBufferSize, alignment> moved_buffer;
const uint32_t wait_index =
WaitForMultipleObjects(static_cast<DWORD>(std::size(watch_handles)),
watch_handles, false, timeout_ms_);
if (wait_index == WAIT_TIMEOUT) {
ScopedHandle dir_handle2(CreateFileW(
Util::Utf8ToWideStr(dir_path_).c_str(), /* watched directory */
FILE_LIST_DIRECTORY, /* rights to list the directory */
FILE_SHARE_READ | FILE_SHARE_WRITE |
FILE_SHARE_DELETE, /* sharing mode */
nullptr, /* security attributes */
OPEN_EXISTING, /* opens a file iff exists */
FILE_FLAG_BACKUP_SEMANTICS |
FILE_FLAG_OVERLAPPED, /* asynchronous I/O */
nullptr)); /* no template file */
FILE_BASIC_INFO dir_info2;
// The directory was removed. The new one was not created.
if (!dir_handle.IsValid() || !dir_handle2.IsValid() ||
!GetFileInformationByHandleEx(dir_handle2.Get(), FileBasicInfo,
&dir_info2, sizeof(dir_info2))) {
SetStatus(absl::FailedPreconditionError(
absl::StrFormat("The watched directory was removed '%s': '%s'",
dir_path_, Util::GetLastWin32Error())));
MaybeSetState(FileWatcherState::kFailed);
break;
}
if (dir_info.CreationTime.QuadPart != dir_info2.CreationTime.QuadPart) {
SetStatus(absl::FailedPreconditionError(absl::StrFormat(
"The watched directory was recreated '%s'", dir_path_)));
MaybeSetState(FileWatcherState::kFailed);
break;
}
// Wait for a new event.
continue;
}
const uint32_t handle_index = wait_index - WAIT_OBJECT_0;
if (handle_index >= std::size(watch_handles)) {
SetStatus(absl::InvalidArgumentError(absl::StrFormat(
"WaitForMultipleObjects failed with invalid handle index %u",
handle_index)));
MaybeSetState(FileWatcherState::kFailed);
break;
}
if (handle_index == shutdown_index) {
ResetEvent(shutdown_event_.Get());
SetStatus(absl::OkStatus());
MaybeSetState(FileWatcherState::kShuttingDown);
break;
}
if (handle_index == read_index) {
DWORD read_bytes = 0;
if (!GetOverlappedResult(dir_handle.Get(), &overlapped, &read_bytes,
TRUE)) {
ResetEvent(overlapped.hEvent);
SetStatus(absl::UnavailableError(
absl::StrFormat("GetOverlappedResult() failed: '%s'",
Util::GetLastWin32Error())));
MaybeSetState(FileWatcherState::kFailed);
break;
}
if (read_bytes == 0) {
SetStatus(absl::DataLossError(absl::StrFormat(
"Buffer overflow: no events were read for the directory '%s'",
dir_path_)));
MaybeSetState(FileWatcherState::kFailed);
break;
}
std::swap(moved_buffer, buffer);
ResetEvent(overlapped.hEvent);
if (read_directory_changes_ex_) {
res = read_directory_changes_ex_(
dir_handle.Get(), &buffer, sizeof(buffer),
true /* check subfolders */, kFileNotificationFlags,
nullptr /* not needed as async read */, &overlapped,
nullptr /* no completion routine */,
ReadDirectoryNotifyExtendedInformation);
ProcessDirChanges<FILE_NOTIFY_EXTENDED_INFORMATION>(&moved_buffer,
read_bytes);
} else {
res = ReadDirectoryChangesW(
dir_handle.Get(), &buffer, sizeof(buffer),
true /* check subfolders */, kFileNotificationFlags,
nullptr /* not needed as async read */, &overlapped,
nullptr /* no completion routine */);
ProcessDirChanges<FILE_NOTIFY_INFORMATION>(&moved_buffer, read_bytes);
}
if (res == FALSE && GetLastError() != ERROR_IO_PENDING) {
SetStatus(absl::FailedPreconditionError(absl::StrFormat(
"Could not read changes in the watched directory '%s': '%s'",
dir_path_, Util::GetLastWin32Error())));
MaybeSetState(FileWatcherState::kFailed);
break;
}
}
}
CancelDirIo(dir_handle, Util::Utf8ToWideStr(dir_path_));
return;
}
// Classifies the changes in the watched directory collected till now,
// deduplicates the changes if needed, and executes the caller's callback.
// BufferType can be FILE_NOTIFY_EXTENDED_INFORMATION or
// FILE_NOTIFY_INFORMATION, depending on whether ReadDirectoryChangesExW is
// available or not.
template <typename BufferType>
void ProcessDirChanges(void* buffer, size_t read_bytes)
ABSL_LOCKS_EXCLUDED(modified_files_mutex_) {
{
absl::MutexLock mutex(&modified_files_mutex_);
assert(buffer);
size_t offset = 0;
FileMap modified_files;
while (offset < read_bytes) {
auto* info = reinterpret_cast<BufferType*>(
reinterpret_cast<char*>(buffer) + offset);
std::string file_name = Util::WideToUtf8Str(std::wstring(
info->FileName,
info->FileName + (info->FileNameLength / sizeof(info->FileName))));
bool was_added = false;
auto iter = modified_files_.find(file_name);
if (iter != modified_files_.end())
was_added = iter->second.action == FileAction::kAdded;
// Merge the current change into |modified_files_| in a way so that
// sequences that use temp files (e.g. ADDED - MODIFIED - REMOVED)
// result in no entry in |modified_files_| at all.
bool remove = false;
FileAction new_action = FileAction::kAdded;
switch (info->Action) {
case FILE_ACTION_REMOVED:
case FILE_ACTION_RENAMED_OLD_NAME:
// If the entry was originally added, remove it again.
if (was_added)
remove = true;
else
new_action = FileAction::kDeleted;
break;
case FILE_ACTION_ADDED:
case FILE_ACTION_RENAMED_NEW_NAME:
new_action = FileAction::kAdded;
break;
case FILE_ACTION_MODIFIED:
// Keep the "added" state if the file was originally added.
new_action = was_added ? FileAction::kAdded : FileAction::kModified;
break;
}
// The file stats are only present if ReadDirectoryChangesEx is present.
// Otherwise, we'll get them later.
bool is_dir = false;
uint64_t size = 0;
int64_t mtime = 0;
if constexpr (std::is_same_v<BufferType,
FILE_NOTIFY_EXTENDED_INFORMATION>) {
is_dir = (info->FileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
size = static_cast<uint64_t>(info->FileSize.QuadPart);
mtime = ToUnixTime(info->LastModificationTime);
}
if (remove) {
modified_files_.erase(iter);
} else {
// Note: insert_or_assign updates earlier entries.
modified_files_.insert_or_assign(
file_name, FileInfo(new_action, is_dir, size, mtime));
}
++event_count_;
// No further entry exists.
if (info->NextEntryOffset == 0) break;
offset += info->NextEntryOffset;
}
}
// Invoke files changed callback if present.
if (files_changed_cb_) files_changed_cb_();
}
void SetStatus(const absl::Status& status)
ABSL_LOCKS_EXCLUDED(status_mutex_) {
LOG_DEBUG("Setting status '%s' of the file watcher process",
status.ToString().c_str());
absl::MutexLock mutex(&status_mutex_);
status_ = status;
}
// Modifies the file watcher state iff it is not shutting down.
void MaybeSetState(FileWatcherState state) ABSL_LOCKS_EXCLUDED(state_mutex_) {
LOG_DEBUG("Setting state %u of the file watcher process", state);
absl::MutexLock mutex(&state_mutex_);
if (state_ != FileWatcherState::kShuttingDown) state_ = state;
}
std::string dir_path_; // the path to the watched directory in UTF8.
ScopedHandle shutdown_event_; // event to shutdown the watcher.
std::thread dir_reader_; // watching thread.
mutable absl::Mutex status_mutex_;
absl::Status status_ = absl::OkStatus() ABSL_GUARDED_BY(status_mutex_);
mutable absl::Mutex modified_files_mutex_;
FileMap modified_files_ ABSL_GUARDED_BY(modified_files_mutex_);
uint32_t event_count_ ABSL_GUARDED_BY(modified_files_mutex_) = 0;
uint32_t dir_recreate_count_ ABSL_GUARDED_BY(modified_files_mutex_) = 0;
mutable absl::Mutex state_mutex_;
FileWatcherState state_ = FileWatcherState::kDefault ABSL_GUARDED_BY(
state_mutex_); // the current watcher state.
// Pointer to ReadDirectoryChangesExW function if available.
decltype(ReadDirectoryChangesExW)* read_directory_changes_ex_ = nullptr;
FilesChangedCb files_changed_cb_; // callback to react on modifications
// inside the watched directory.
DirRecreatedCb dir_recreated_cb_; // callback to react on the
// creation/removal/re-creation of the
// watched directory. It is not guarantee
// that the watched directory exists.
unsigned int timeout_ms_; // timeout in ms to wait for a change.
};
FileWatcherWin::FileWatcherWin(std::string directory) : dir_path_(directory) {}
FileWatcherWin::~FileWatcherWin() {
absl::Status status = StopWatching();
if (!status.ok()) {
LOG_WARNING("Failed to stop watching files: %s", status.ToString());
}
}
FileWatcherWin::FileMap FileWatcherWin::GetModifiedFiles() {
absl::MutexLock mutex(&modified_files_mutex_);
FileMap files;
modified_files_.swap(files);
if (async_watcher_) {
for (const auto& [path, info] : async_watcher_->GetModifiedFiles())
files.insert_or_assign(path, info);
}
return files;
}
absl::Status FileWatcherWin::StartWatching(FilesChangedCb files_changed_cb,
DirRecreatedCb dir_recreated_cb,
unsigned int timeout_ms) {
LOG_INFO("Starting the file watcher");
async_watcher_ = std::make_unique<AsyncFileWatcher>(
dir_path_, std::move(files_changed_cb), std::move(dir_recreated_cb),
timeout_ms, enforceLegacyReadDirectoryChangesForTesting_);
while (GetStatus().ok() && !IsWatching()) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
return GetStatus();
}
absl::Status FileWatcherWin::StopWatching() {
LOG_INFO("Stopping the file watcher");
if (!async_watcher_) {
return absl::OkStatus();
}
async_watcher_->Shutdown();
absl::MutexLock mutex(&modified_files_mutex_);
FileMap files = async_watcher_->GetModifiedFiles();
if (modified_files_.empty()) {
modified_files_.swap(files);
} else {
for (const auto& [path, info] : files)
modified_files_.insert_or_assign(path, info);
}
absl::Status async_status = async_watcher_->GetStatus();
async_watcher_.reset();
return async_status;
}
bool FileWatcherWin::IsWatching() const {
return async_watcher_ ? async_watcher_->IsWatching() : false;
}
absl::Status FileWatcherWin::GetStatus() const {
return async_watcher_ ? async_watcher_->GetStatus() : absl::OkStatus();
}
uint32_t FileWatcherWin::GetEventCountForTesting() const {
return async_watcher_ ? async_watcher_->GetEventCount() : 0;
}
uint32_t FileWatcherWin::GetDirRecreateEventCountForTesting() const {
return async_watcher_ ? async_watcher_->GetDirRecreateEventCount() : 0;
}
void FileWatcherWin::EnforceLegacyReadDirectoryChangesForTesting() {
assert(!IsWatching());
enforceLegacyReadDirectoryChangesForTesting_ = true;
}
} // namespace cdc_ft

111
common/file_watcher_win.h 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.
*/
#ifndef COMMON_FILE_WATCHER_WIN_H_
#define COMMON_FILE_WATCHER_WIN_H_
#define WIN32_LEAN_AND_MEAN
#include <string>
#include <unordered_map>
#include "absl/base/thread_annotations.h"
#include "absl/status/status.h"
#include "absl/synchronization/mutex.h"
namespace cdc_ft {
class AsyncFileWatcher;
// FileWatcherWin observes changes done in a specific directory on Windows.
class FileWatcherWin {
public:
// Default timeout in milliseconds.
static constexpr unsigned int kFileWatcherTimeoutMs = 1000;
using FilesChangedCb = std::function<void()>;
using DirRecreatedCb = std::function<void()>;
enum class FileAction { kAdded, kModified, kDeleted };
struct FileInfo {
FileAction action;
bool is_dir;
uint64_t size;
int64_t mtime;
FileInfo(FileAction action, bool is_dir, uint64_t size, int64_t mtime)
: action(action), is_dir(is_dir), size(size), mtime(mtime) {}
};
using FileMap = std::unordered_map<std::string, FileInfo>;
explicit FileWatcherWin(std::string directory);
FileWatcherWin(const FileWatcherWin& other) = delete;
FileWatcherWin& operator=(const FileWatcherWin& other) = delete;
~FileWatcherWin();
// Returns a map that maps relative paths of modified files to their file
// attributes.
FileMap GetModifiedFiles() ABSL_LOCKS_EXCLUDED(modified_files_mutex_);
// Starts watching directory changes.
// |files_changed_cb| is called on a background thread whenever files changed.
// |dir_recreated_cb| is called on a background thread whenever the watched
// directory was removed/created/re-created. It does not guarantee that the
// watched directory exists. The caller should check on its own and handle
// a case if the directory still does not exist. The callback shows that all
// outstanding changes are not valid anymore.
// |timeout_ms| is a timeout in ms for a change in the watched directory.
absl::Status StartWatching(FilesChangedCb files_changed_cb = FilesChangedCb(),
DirRecreatedCb dir_recreated_cb = DirRecreatedCb(),
unsigned int timeout_ms = kFileWatcherTimeoutMs);
// Stops watching directory changes.
absl::Status StopWatching() ABSL_LOCKS_EXCLUDED(modified_files_mutex_);
// Indicates whether a directory is currently watched.
bool IsWatching() const;
// Returns the watching status.
absl::Status GetStatus() const;
// Returns the total file changed events received so far.
// Returns 0 if the watcher is currently in stopped state.
uint32_t GetEventCountForTesting() const;
// Returns the total number of events for the directory changes received so
// far. Returns 0 if the watcher is currently in stopped state.
uint32_t GetDirRecreateEventCountForTesting() const;
// Enforces the use of ReadDirectoryChanges instead of ReadDirectoryChangesEx.
// Must be called before StartWatching.
void EnforceLegacyReadDirectoryChangesForTesting();
private:
std::string dir_path_;
std::unique_ptr<AsyncFileWatcher> async_watcher_;
absl::Mutex modified_files_mutex_;
FileMap modified_files_ ABSL_GUARDED_BY(modified_files_mutex_);
bool enforceLegacyReadDirectoryChangesForTesting_ = false;
};
} // namespace cdc_ft
#endif // COMMON_FILE_WATCHER_WIN_H_

View File

@@ -0,0 +1,609 @@
// 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 "common/file_watcher_win.h"
#define WIN32_LEAN_AND_MEAN
#include <chrono>
#include <string>
#include <thread>
#include "absl/strings/match.h"
#include "common/buffer.h"
#include "common/log.h"
#include "common/path.h"
#include "common/platform.h"
#include "common/status_test_macros.h"
#include "common/util.h"
#include "gtest/gtest.h"
namespace cdc_ft {
const char* ActionToString(const FileWatcherWin::FileAction& fa) {
switch (fa) {
case FileWatcherWin::FileAction::kAdded:
return "ADDED";
case FileWatcherWin::FileAction::kModified:
return "MODIFIED";
case FileWatcherWin::FileAction::kDeleted:
return "DELETED";
default:
return "UNKNOWN";
}
}
std::ostream& operator<<(std::ostream& os,
const FileWatcherWin::FileMap& files) {
for (const auto& [path, fi] : files) {
os << "path=" << path << ", action=" << ActionToString(fi.action)
<< ", is_dir=" << fi.is_dir << ", mtime=" << fi.mtime
<< ", size=" << fi.size << std::endl;
}
return os;
}
namespace {
constexpr char kWatcherTestDir[] = "watcher_test_dir";
constexpr char kWatcherWatchedDir[] = "watcher_watched_dir";
constexpr char kFirstData[] = {10, 20, 30, 40, 50, 60, 70, 80, 90};
constexpr char kSecondData[] = {100, 101, 102, 103, 104, 105, 106, 107, 108,
100, 101, 102, 103, 104, 105, 106, 107, 108};
constexpr size_t kFirstDataSize = sizeof(kFirstData);
constexpr size_t kSecondDataSize = sizeof(kSecondData);
constexpr char kFirstFile[] = "first_test_file.txt";
constexpr char kSecondFile[] = "second_test_file.txt";
constexpr char kFirstDir[] = "first_test_dir";
constexpr char kSecondDir[] = "second_test_dir";
constexpr bool kFile = false;
constexpr bool kDir = true;
constexpr absl::Duration kWaitTimeout = absl::Seconds(5);
constexpr unsigned int kFWTimeout = 10;
using FileMap = FileWatcherWin::FileMap;
using FileAction = FileWatcherWin::FileAction;
using FileInfo = FileWatcherWin::FileInfo;
class FileWatcherParameterizedTest : public ::testing::TestWithParam<bool> {
public:
FileWatcherParameterizedTest() : watcher_(watcher_dir_path_) {
Log::Initialize(std::make_unique<ConsoleLog>(LogLevel::kInfo));
}
~FileWatcherParameterizedTest() { Log::Shutdown(); }
void SetUp() override {
legacyReadDirectoryChanges_ = GetParam();
if (legacyReadDirectoryChanges_)
watcher_.EnforceLegacyReadDirectoryChangesForTesting();
EXPECT_OK(path::RemoveDirRec(watcher_dir_path_));
EXPECT_OK(path::CreateDirRec(watcher_dir_path_));
}
void TearDown() override { EXPECT_OK(path::RemoveDirRec(watcher_dir_path_)); }
// True if ReadDirectoryChangesW should be enforced (instead of *ExW).
bool legacyReadDirectoryChanges_ = false;
protected:
void OnFilesChanged() {
absl::MutexLock lock(&files_changed_mutex_);
files_changed_ = true;
}
void OnDirRecreated() {
absl::MutexLock lock(&files_changed_mutex_);
dir_recreated_ = true;
}
bool WaitForChange(uint32_t min_event_count = 0) {
absl::MutexLock lock(&files_changed_mutex_);
bool changed = false;
do {
auto cond = [this]() { return files_changed_; };
files_changed_mutex_.AwaitWithTimeout(absl::Condition(&cond),
kWaitTimeout);
changed = files_changed_;
files_changed_ = false;
} while (changed && watcher_.GetEventCountForTesting() < min_event_count);
return changed;
}
bool WaitForDirRecreated(uint32_t min_event_count = 0) {
absl::MutexLock lock(&files_changed_mutex_);
bool changed = false;
do {
auto cond = [this]() { return dir_recreated_; };
files_changed_mutex_.AwaitWithTimeout(absl::Condition(&cond),
kWaitTimeout);
changed = dir_recreated_;
dir_recreated_ = false;
} while (changed &&
watcher_.GetDirRecreateEventCountForTesting() < min_event_count);
return changed;
}
FileMap GetChangedFiles(size_t number_of_files) {
FileMap modified_files;
// Wait for events, until they are processed.
while (modified_files.size() < number_of_files) {
EXPECT_TRUE(WaitForChange());
for (const auto& [path, info] : watcher_.GetModifiedFiles())
modified_files.insert_or_assign(path, info);
}
return modified_files;
}
void ExpectFile(const FileMap& modified_files, const std::string& path) {
EXPECT_TRUE(modified_files.find(path) != modified_files.end())
<< path << " is missing from " << std::endl
<< modified_files;
}
void ExpectFileInfo(const FileMap& modified_files, const std::string& path,
FileAction action, bool is_dir, uint64_t size) {
auto iter = modified_files.find(path);
EXPECT_TRUE(iter != modified_files.end())
<< path << " is missing from " << std::endl
<< modified_files;
if (iter != modified_files.end()) {
EXPECT_EQ(iter->second.action, action);
// is_dir and size are not available for the legacy ReadDirectoryChangesW,
// but that data isn't needed, anyway.
if (action != FileAction::kDeleted || !legacyReadDirectoryChanges_) {
EXPECT_EQ(iter->second.is_dir, is_dir);
EXPECT_EQ(iter->second.size, size);
}
// Don't bother checking mtime here, it's checked elsewhere.
}
}
const std::string test_dir_path_ =
path::Join(path::GetTempDir(), kWatcherTestDir);
const std::string watcher_dir_path_ =
path::Join(test_dir_path_, kWatcherWatchedDir);
const std::string first_file_path_ =
path::Join(watcher_dir_path_, kFirstFile);
const std::string second_file_path_ =
path::Join(watcher_dir_path_, kSecondFile);
const std::string first_dir_path_ = path::Join(watcher_dir_path_, kFirstDir);
const std::string second_dir_path_ =
path::Join(watcher_dir_path_, kSecondDir);
FileWatcherWin watcher_;
bool files_changed_ ABSL_GUARDED_BY(files_changed_mutex_) = false;
bool dir_recreated_ ABSL_GUARDED_BY(files_changed_mutex_) = false;
absl::Mutex files_changed_mutex_;
};
TEST_P(FileWatcherParameterizedTest, DirDoesNotExist) {
FileWatcherWin watcher("non-existing folder");
if (legacyReadDirectoryChanges_)
watcher_.EnforceLegacyReadDirectoryChangesForTesting();
EXPECT_NOT_OK(watcher.StartWatching([this]() { OnFilesChanged(); }));
EXPECT_FALSE(watcher.IsWatching());
absl::Status status = watcher.GetStatus();
EXPECT_NOT_OK(status);
EXPECT_TRUE(absl::IsFailedPrecondition(status));
EXPECT_TRUE(absl::StrContains(status.message(), "Could not start watching"));
}
TEST_P(FileWatcherParameterizedTest, CreateFile) {
EXPECT_OK(watcher_.StartWatching([this]() { OnFilesChanged(); }));
EXPECT_OK(path::WriteFile(first_file_path_, kFirstData, kFirstDataSize));
FileMap modified_files = GetChangedFiles(1u);
EXPECT_EQ(modified_files.size(), 1u);
ExpectFile(modified_files, kFirstFile);
EXPECT_OK(watcher_.StopWatching());
}
TEST_P(FileWatcherParameterizedTest, CreateFileDelayed) {
EXPECT_OK(watcher_.StartWatching([this]() { OnFilesChanged(); }));
std::this_thread::sleep_for(std::chrono::milliseconds(20));
EXPECT_OK(path::WriteFile(first_file_path_, kFirstData, kFirstDataSize));
FileMap modified_files = GetChangedFiles(1u);
EXPECT_EQ(modified_files.size(), 1u);
ExpectFile(modified_files, kFirstFile);
EXPECT_OK(watcher_.StopWatching());
}
TEST_P(FileWatcherParameterizedTest, CreateTwoFiles) {
EXPECT_OK(watcher_.StartWatching([this]() { OnFilesChanged(); }));
EXPECT_OK(path::WriteFile(first_file_path_, kFirstData, kFirstDataSize));
EXPECT_OK(path::WriteFile(second_file_path_, kFirstData, kFirstDataSize));
FileMap modified_files = GetChangedFiles(2u);
EXPECT_EQ(modified_files.size(), 2u);
ExpectFile(modified_files, kFirstFile);
ExpectFile(modified_files, kSecondFile);
EXPECT_OK(watcher_.StopWatching());
}
TEST_P(FileWatcherParameterizedTest, CreateDir) {
EXPECT_OK(watcher_.StartWatching([this]() { OnFilesChanged(); }));
EXPECT_OK(path::CreateDir(first_dir_path_));
FileMap modified_files = GetChangedFiles(1u);
EXPECT_EQ(modified_files.size(), 1u);
ExpectFile(modified_files, kFirstDir);
EXPECT_OK(watcher_.StopWatching());
}
TEST_P(FileWatcherParameterizedTest, RenameDir) {
EXPECT_OK(path::CreateDir(first_dir_path_));
EXPECT_OK(watcher_.StartWatching([this]() { OnFilesChanged(); }));
EXPECT_OK(path::RenameFile(first_dir_path_, second_dir_path_));
FileMap modified_files = GetChangedFiles(2u);
EXPECT_EQ(modified_files.size(), 2u);
ExpectFileInfo(modified_files, kFirstDir, FileAction::kDeleted, kDir, 0);
ExpectFileInfo(modified_files, kSecondDir, FileAction::kAdded, kDir, 0);
EXPECT_OK(watcher_.StopWatching());
}
TEST_P(FileWatcherParameterizedTest, RenameFile) {
EXPECT_OK(path::WriteFile(first_file_path_, kFirstData, kFirstDataSize));
EXPECT_OK(watcher_.StartWatching([this]() { OnFilesChanged(); }));
EXPECT_OK(path::RenameFile(first_file_path_, second_file_path_));
FileMap modified_files = GetChangedFiles(2u);
EXPECT_EQ(modified_files.size(), 2u);
ExpectFileInfo(modified_files, kFirstFile, FileAction::kDeleted, kFile,
kFirstDataSize);
ExpectFileInfo(modified_files, kSecondFile, FileAction::kAdded, kFile,
kFirstDataSize);
EXPECT_OK(watcher_.StopWatching());
}
TEST_P(FileWatcherParameterizedTest, RemoveDir) {
EXPECT_OK(path::CreateDir(first_dir_path_));
EXPECT_OK(watcher_.StartWatching([this]() { OnFilesChanged(); }));
EXPECT_OK(path::RemoveDirRec(first_dir_path_));
FileMap modified_files = GetChangedFiles(1u);
EXPECT_EQ(modified_files.size(), 1u);
ExpectFile(modified_files, kFirstDir);
EXPECT_OK(watcher_.StopWatching());
}
TEST_P(FileWatcherParameterizedTest, RemoveFile) {
EXPECT_OK(path::WriteFile(first_file_path_, kFirstData, kFirstDataSize));
EXPECT_OK(watcher_.StartWatching([this]() { OnFilesChanged(); }));
EXPECT_OK(path::RemoveFile(first_file_path_));
FileMap modified_files = GetChangedFiles(1u);
EXPECT_EQ(modified_files.size(), 1u);
ExpectFile(modified_files, kFirstFile);
EXPECT_OK(watcher_.StopWatching());
}
TEST_P(FileWatcherParameterizedTest, ChangeFile) {
EXPECT_OK(path::WriteFile(first_file_path_, kFirstData, kFirstDataSize));
EXPECT_OK(watcher_.StartWatching([this]() { OnFilesChanged(); }));
EXPECT_OK(path::WriteFile(first_file_path_, kSecondData, kSecondDataSize));
FileMap modified_files = GetChangedFiles(1u);
EXPECT_EQ(modified_files.size(), 1u);
ExpectFile(modified_files, kFirstFile);
EXPECT_OK(watcher_.StopWatching());
}
TEST_P(FileWatcherParameterizedTest, DirHierarchy) {
EXPECT_OK(watcher_.StartWatching([this]() { OnFilesChanged(); }));
std::vector<std::string> files = {"1.txt", "2.txt", "3.txt", "4.txt",
"5.txt", "6.txt", "7.txt", "8.txt"};
EXPECT_OK(path::CreateDir(first_dir_path_));
EXPECT_OK(path::CreateDir(second_dir_path_));
for (const std::string& file : files) {
for (const auto& path :
{first_dir_path_, second_dir_path_, watcher_dir_path_})
EXPECT_OK(
path::WriteFile(path::Join(path, file), kFirstData, kFirstDataSize));
}
FileMap modified_files = GetChangedFiles(files.size() * 3 + 2);
ASSERT_EQ(modified_files.size(), files.size() * 3 + 2);
for (const std::string& file : files) {
ExpectFile(modified_files, path::Join(kFirstDir, file));
ExpectFile(modified_files, path::Join(kSecondDir, file));
ExpectFile(modified_files, file);
}
ExpectFile(modified_files, kFirstDir);
ExpectFile(modified_files, kSecondDir);
EXPECT_OK(watcher_.StopWatching());
}
TEST_P(FileWatcherParameterizedTest, NoReadDirChanges) {
EXPECT_OK(watcher_.StartWatching([this]() { OnFilesChanged(); }));
EXPECT_OK(watcher_.StopWatching());
FileMap modified_files = GetChangedFiles(0u);
EXPECT_EQ(modified_files.size(), 0u);
}
TEST_P(FileWatcherParameterizedTest, RestartWatchingNoChanges) {
EXPECT_OK(watcher_.StartWatching([this]() { OnFilesChanged(); }));
EXPECT_OK(watcher_.StopWatching());
// Restart with reading.
EXPECT_OK(watcher_.StartWatching([this]() { OnFilesChanged(); }));
EXPECT_OK(watcher_.StopWatching());
// Restart without reading.
EXPECT_OK(watcher_.StartWatching([this]() { OnFilesChanged(); }));
EXPECT_OK(watcher_.StopWatching());
}
TEST_P(FileWatcherParameterizedTest, RestartWatchingWithChanges) {
EXPECT_OK(watcher_.StartWatching([this]() { OnFilesChanged(); }));
EXPECT_OK(path::WriteFile(first_file_path_, kFirstData, kFirstDataSize));
EXPECT_OK(watcher_.StopWatching());
// first_test_dir should not be in the modification set.
EXPECT_OK(path::CreateDir(first_dir_path_));
EXPECT_OK(watcher_.StartWatching([this]() { OnFilesChanged(); }));
EXPECT_OK(path::WriteFile(second_file_path_, kSecondData, kSecondDataSize));
FileMap modified_files = GetChangedFiles(2u);
EXPECT_EQ(modified_files.size(), 2u);
ExpectFile(modified_files, kFirstFile);
ExpectFile(modified_files, kSecondFile);
EXPECT_OK(watcher_.StopWatching());
}
TEST_P(FileWatcherParameterizedTest, ReadFileNoNotification) {
EXPECT_OK(path::WriteFile(first_file_path_, kFirstData, kFirstDataSize));
EXPECT_OK(watcher_.StartWatching([this]() { OnFilesChanged(); }));
Buffer data;
EXPECT_OK(path::ReadFile(first_file_path_, &data));
FileMap modified_files = GetChangedFiles(0u);
EXPECT_EQ(modified_files.size(), 0u);
EXPECT_OK(watcher_.StopWatching());
}
TEST_P(FileWatcherParameterizedTest, SearchFilesNoNotification) {
EXPECT_OK(path::WriteFile(first_file_path_, kFirstData, kFirstDataSize));
EXPECT_OK(watcher_.StartWatching([this]() { OnFilesChanged(); }));
unsigned int counter = 0;
auto handler = [&counter](const std::string& /*dir*/,
const std::string& /*filename*/,
int64_t /*modified_time*/, uint64_t /*size*/,
bool /*is_directory*/) {
++counter;
return absl::OkStatus();
};
EXPECT_OK(path::SearchFiles(first_file_path_, true, handler));
EXPECT_EQ(counter, 1u);
FileMap modified_files = GetChangedFiles(0u);
EXPECT_EQ(modified_files.size(), 0u);
EXPECT_OK(watcher_.StopWatching());
}
TEST_P(FileWatcherParameterizedTest, ActionAdd) {
EXPECT_OK(watcher_.StartWatching([this]() { OnFilesChanged(); }));
EXPECT_OK(path::WriteFile(first_file_path_, kFirstData, kFirstDataSize));
EXPECT_TRUE(WaitForChange(/*min_event_count=*/2)); // 1x add, 1x modify
FileMap modified_files = watcher_.GetModifiedFiles();
EXPECT_EQ(modified_files.size(), 1u);
ExpectFileInfo(modified_files, kFirstFile, FileAction::kAdded, kFile,
kFirstDataSize);
EXPECT_OK(watcher_.StopWatching());
}
TEST_P(FileWatcherParameterizedTest, ActionModify) {
EXPECT_OK(path::WriteFile(first_file_path_, kFirstData, kFirstDataSize));
EXPECT_OK(watcher_.StartWatching([this]() { OnFilesChanged(); }));
EXPECT_OK(path::WriteFile(first_file_path_, kSecondData, kSecondDataSize));
EXPECT_TRUE(WaitForChange(/*min_event_count=*/2)); // 2x modify
FileMap modified_files = watcher_.GetModifiedFiles();
EXPECT_EQ(modified_files.size(), 1u);
ExpectFileInfo(modified_files, kFirstFile, FileAction::kModified, kFile,
kSecondDataSize);
EXPECT_OK(watcher_.StopWatching());
}
TEST_P(FileWatcherParameterizedTest, ActionAddModify) {
EXPECT_OK(watcher_.StartWatching([this]() { OnFilesChanged(); }));
EXPECT_OK(path::WriteFile(first_file_path_, kFirstData, kFirstDataSize));
EXPECT_OK(path::WriteFile(first_file_path_, kSecondData, kSecondDataSize));
EXPECT_TRUE(WaitForChange(/*min_event_count=*/4)); // 1x add, 3x modify
// Add + modify should not result in FileAction::kModified.
FileMap modified_files = watcher_.GetModifiedFiles();
EXPECT_EQ(modified_files.size(), 1u);
ExpectFileInfo(modified_files, kFirstFile, FileAction::kAdded, kFile,
kSecondDataSize);
EXPECT_OK(watcher_.StopWatching());
}
TEST_P(FileWatcherParameterizedTest, ActionDelete) {
EXPECT_OK(path::WriteFile(first_file_path_, kFirstData, kFirstDataSize));
EXPECT_OK(watcher_.StartWatching([this]() { OnFilesChanged(); }));
EXPECT_OK(path::RemoveFile(first_file_path_));
EXPECT_TRUE(WaitForChange(/*min_event_count=*/1)); // 1x remove
FileMap modified_files = watcher_.GetModifiedFiles();
EXPECT_EQ(modified_files.size(), 1u);
ExpectFileInfo(modified_files, kFirstFile, FileAction::kDeleted, kFile,
kFirstDataSize);
EXPECT_OK(watcher_.StopWatching());
}
TEST_P(FileWatcherParameterizedTest, ActionAddModifyRemove) {
EXPECT_OK(watcher_.StartWatching([this]() { OnFilesChanged(); }));
EXPECT_OK(path::WriteFile(first_file_path_, kFirstData, kFirstDataSize));
EXPECT_OK(path::WriteFile(first_file_path_, kSecondData, kSecondDataSize));
EXPECT_OK(path::RemoveFile(first_file_path_));
EXPECT_TRUE(
WaitForChange(/*min_event_count=*/5)); // 1x add, 3x modify, 1x remove
// The watcher should collapse add-modify-remove sequences and report no
// changes.
FileMap modified_files = watcher_.GetModifiedFiles();
EXPECT_EQ(modified_files.size(), 0u);
EXPECT_OK(watcher_.StopWatching());
}
TEST_P(FileWatcherParameterizedTest, ActionModifyRemove) {
EXPECT_OK(path::WriteFile(first_file_path_, kFirstData, kFirstDataSize));
EXPECT_OK(watcher_.StartWatching([this]() { OnFilesChanged(); }));
EXPECT_OK(path::WriteFile(first_file_path_, kSecondData, kSecondDataSize));
EXPECT_OK(path::RemoveFile(first_file_path_));
EXPECT_TRUE(WaitForChange(/*min_event_count=*/3)); // 2x modify, 1x remove
FileMap modified_files = watcher_.GetModifiedFiles();
EXPECT_EQ(modified_files.size(), 1u);
ExpectFileInfo(modified_files, kFirstFile, FileAction::kDeleted, kFile,
kSecondDataSize);
EXPECT_OK(watcher_.StopWatching());
}
TEST_P(FileWatcherParameterizedTest, ModifiedTime) {
EXPECT_OK(watcher_.StartWatching([this]() { OnFilesChanged(); }));
EXPECT_OK(path::WriteFile(first_file_path_, kFirstData, kFirstDataSize));
EXPECT_TRUE(WaitForChange(/*min_event_count=*/2)); // 2x modify
FileMap modified_files = watcher_.GetModifiedFiles();
ASSERT_EQ(modified_files.size(), 1u);
time_t mtime;
EXPECT_OK(path::GetFileTime(first_file_path_, &mtime));
const FileInfo& info = modified_files.begin()->second;
EXPECT_EQ(info.mtime, mtime);
EXPECT_OK(watcher_.StopWatching());
}
TEST_P(FileWatcherParameterizedTest, DeleteWatchedDir) {
EXPECT_OK(watcher_.StartWatching([this]() { OnFilesChanged(); },
[this]() { OnDirRecreated(); }));
EXPECT_OK(path::RemoveDirRec(watcher_dir_path_));
EXPECT_TRUE(WaitForDirRecreated(1u));
EXPECT_TRUE(watcher_.GetModifiedFiles().empty());
EXPECT_NOT_OK(watcher_.GetStatus());
// The error status should not be overwritten.
EXPECT_NOT_OK(watcher_.StopWatching());
}
TEST_P(FileWatcherParameterizedTest, RecreateWatchedDir) {
EXPECT_OK(watcher_.StartWatching([this]() { OnFilesChanged(); },
[this]() { OnDirRecreated(); }, kFWTimeout));
EXPECT_OK(path::RemoveDirRec(watcher_dir_path_));
EXPECT_TRUE(WaitForDirRecreated(1u));
EXPECT_OK(path::CreateDirRec(watcher_dir_path_));
EXPECT_TRUE(WaitForDirRecreated(2u));
EXPECT_TRUE(watcher_.GetModifiedFiles().empty());
EXPECT_OK(watcher_.GetStatus());
// Creation of a new file should be detected.
EXPECT_OK(path::WriteFile(first_file_path_, kFirstData, kFirstDataSize));
FileMap modified_files = GetChangedFiles(1u);
EXPECT_EQ(modified_files.size(), 1u);
ExpectFile(modified_files, kFirstFile);
EXPECT_OK(watcher_.StopWatching());
}
TEST_P(FileWatcherParameterizedTest, RecreateUpperDir) {
EXPECT_OK(watcher_.StartWatching([this]() { OnFilesChanged(); },
[this]() { OnDirRecreated(); }, kFWTimeout));
// Initially, there should be no dir_recreated_events.
EXPECT_EQ(watcher_.GetDirRecreateEventCountForTesting(), 0u);
EXPECT_OK(path::RemoveDirRec(test_dir_path_));
EXPECT_TRUE(WaitForDirRecreated(1u));
// Only 1 event should exist (for directory removal).
EXPECT_OK(path::CreateDirRec(test_dir_path_));
EXPECT_OK(path::CreateDirRec(watcher_dir_path_));
// As the upper level directory is created separately, no new event should be
// available.
EXPECT_TRUE(WaitForDirRecreated(2u));
// Only 1 additional event should be registered for creation of the directory.
EXPECT_EQ(watcher_.GetDirRecreateEventCountForTesting(), 2u);
EXPECT_TRUE(watcher_.GetModifiedFiles().empty());
EXPECT_OK(watcher_.GetStatus());
// Creation of a new file should be detected.
EXPECT_OK(path::WriteFile(first_file_path_, kFirstData, kFirstDataSize));
FileMap modified_files = GetChangedFiles(1u);
EXPECT_EQ(modified_files.size(), 1u);
ExpectFile(modified_files, kFirstFile);
// No new events should be registered for the watched directory.
EXPECT_EQ(watcher_.GetDirRecreateEventCountForTesting(), 2u);
EXPECT_OK(watcher_.StopWatching());
}
TEST_P(FileWatcherParameterizedTest, RecreateWatchedDirNoOldChanges) {
EXPECT_OK(watcher_.StartWatching([this]() { OnFilesChanged(); },
[this]() { OnDirRecreated(); }, kFWTimeout));
EXPECT_OK(path::WriteFile(first_file_path_, kFirstData, kFirstDataSize));
EXPECT_OK(path::RemoveDirRec(watcher_dir_path_));
EXPECT_OK(path::CreateDirRec(watcher_dir_path_));
EXPECT_TRUE(WaitForDirRecreated(2u));
EXPECT_TRUE(watcher_.GetModifiedFiles().empty());
EXPECT_OK(watcher_.GetStatus());
EXPECT_OK(watcher_.StopWatching());
}
struct TestName {
std::string operator()(const testing::TestParamInfo<bool>& legacy) const {
return legacy.param ? "ReadDirectoryChangesW" : "ReadDirectoryChangesExW";
}
};
// Run without and with legacy ReadDirectoryChangesW function.
INSTANTIATE_TEST_CASE_P(FileWatcherTest, FileWatcherParameterizedTest,
::testing::Values(false, true), TestName());
} // namespace
} // namespace cdc_ft

View File

@@ -0,0 +1,92 @@
// 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 "common/gamelet_component.h"
#include <cinttypes>
#include "absl/strings/str_format.h"
#include "absl/strings/str_split.h"
#include "common/path.h"
#include "common/status.h"
namespace cdc_ft {
GameletComponent::GameletComponent(std::string filename, uint64_t size,
time_t modified_time)
: filename(filename), size(size), modified_time(modified_time) {}
GameletComponent::~GameletComponent() = default;
bool GameletComponent::operator==(const GameletComponent& other) const {
return filename == other.filename && size == other.size &&
modified_time == other.modified_time;
}
bool GameletComponent::operator!=(const GameletComponent& other) const {
return !(*this == other);
}
// static
absl::Status GameletComponent::Get(
const std::vector<std::string> component_paths,
std::vector<GameletComponent>* components) {
components->clear();
for (const std::string& path : component_paths) {
path::Stats stats;
absl::Status status = path::GetStats(path, &stats);
if (!status.ok())
return WrapStatus(status, "GetStats() failed for '%s'", path);
components->emplace_back(path::BaseName(path), stats.size,
stats.modified_time);
}
return absl::OkStatus();
}
// static
std::string GameletComponent::ToCommandLineArgs(
const std::vector<GameletComponent>& components) {
std::string args;
for (const GameletComponent& comp : components) {
args +=
absl::StrFormat("%s%s %u %d", args.empty() ? "" : " ",
comp.filename.c_str(), comp.size, comp.modified_time);
}
return args;
}
// static
std::vector<GameletComponent> GameletComponent::FromCommandLineArgs(
int argc, const char** argv) {
std::vector<GameletComponent> components;
for (int n = 0; n + 2 < argc; n += 3) {
components.emplace_back(argv[n], std::stol(argv[n + 1]),
std::stol(argv[n + 2]));
}
return components;
}
// static
std::vector<GameletComponent> GameletComponent::FromCommandLineArgs(
const std::string& components_arg) {
std::vector<std::string> args_vec = absl::StrSplit(components_arg, ' ');
int argc = static_cast<int>(args_vec.size());
std::vector<const char*> argv;
for (const std::string& arg : args_vec) argv.push_back(arg.c_str());
return FromCommandLineArgs(argc, argv.data());
}
} // namespace cdc_ft

View File

@@ -0,0 +1,70 @@
/*
* 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 COMMON_GAMELET_COMPONENT_H_
#define COMMON_GAMELET_COMPONENT_H_
#include <string>
#include <vector>
#include "absl/status/status.h"
namespace cdc_ft {
// Provides functionality to check the freshness of gamelet components.
// The components are considered fresh if both the timestamp and the file size
// match.
struct GameletComponent {
std::string filename;
uint64_t size;
int64_t modified_time;
GameletComponent(std::string filename, uint64_t size, time_t modified_time);
~GameletComponent();
bool operator==(const GameletComponent& other) const;
bool operator!=(const GameletComponent& other) const;
// Gets the list of gamelet |components| from the given |component_paths|.
// Returns false if any of the components does not exist or cannot be stat'ed.
static absl::Status Get(const std::vector<std::string> component_paths,
std::vector<GameletComponent>* components);
// Serializes the list of gamelet |components| as command line options.
// This should be called on the workstation. The resulting command line
// should be passed to the gamelet process.
static std::string ToCommandLineArgs(
const std::vector<GameletComponent>& components);
// Deserializes the list of gamelet components from command line options.
// This should be called in the gamelet process to retrieve the workstation
// components, which can then be compared to the gamelet components to check
// freshness.
static std::vector<GameletComponent> FromCommandLineArgs(int argc,
const char** argv);
// Deserializes the list of gamelet components from a |components_arg| string
// as it is returned by ToCommandLineArgs().
// This should be called in the gamelet process to retrieve the workstation
// components, which can then be compared to the gamelet components to check
// freshness.
static std::vector<GameletComponent> FromCommandLineArgs(
const std::string& components_arg);
};
} // namespace cdc_ft
#endif // COMMON_GAMELET_COMPONENT_H_

View File

@@ -0,0 +1,124 @@
// 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 "common/gamelet_component.h"
#include "absl/strings/str_split.h"
#include "common/log.h"
#include "common/path.h"
#include "common/status_test_macros.h"
#include "common/test_main.h"
#include "gtest/gtest.h"
namespace cdc_ft {
namespace {
class GameletComponentTest : public ::testing::Test {
public:
void SetUp() override {
Log::Initialize(std::make_unique<ConsoleLog>(LogLevel::kInfo));
}
void TearDown() override { Log::Shutdown(); }
protected:
std::string base_dir_ = GetTestDataDir("gamelet_component");
std::string valid_component_path_ =
path::Join(base_dir_, "valid", "cdc_rsync_server");
std::string invalid_component_path_ =
path::Join(base_dir_, "invalid", "cdc_rsync_server");
std::string other_component_path_ =
path::Join(base_dir_, "other", "cdc_rsync_server");
};
TEST_F(GameletComponentTest, EqualityOperators) {
constexpr uint64_t size1 = 1001;
constexpr uint64_t size2 = 1002;
constexpr int64_t modified_time1 = 5001;
constexpr int64_t modified_time2 = 5002;
GameletComponent a("file1", size1, modified_time1);
GameletComponent b = a;
EXPECT_TRUE(a == b && !(a != b));
b.filename = "file2";
EXPECT_TRUE(!(a == b) && a != b);
b = a;
b.size = size2;
EXPECT_TRUE(!(a == b) && a != b);
b = a;
b.modified_time = modified_time2;
EXPECT_TRUE(!(a == b) && a != b);
}
TEST_F(GameletComponentTest, GetValidComponents) {
std::vector<GameletComponent> components;
EXPECT_OK(GameletComponent::Get({valid_component_path_}, &components));
ASSERT_EQ(components.size(), 1);
EXPECT_EQ(components[0].filename, "cdc_rsync_server");
EXPECT_GT(components[0].size, 0);
EXPECT_GT(components[0].modified_time, 0);
}
TEST_F(GameletComponentTest, GetInvalidComponents) {
std::vector<GameletComponent> components;
EXPECT_NOT_OK(GameletComponent::Get({invalid_component_path_}, &components));
}
TEST_F(GameletComponentTest, GetChangedComponents) {
std::vector<GameletComponent> components;
EXPECT_OK(GameletComponent::Get({valid_component_path_}, &components));
std::vector<GameletComponent> other_components;
EXPECT_OK(GameletComponent::Get({other_component_path_}, &other_components));
// Force equal timestamps, so that we don't depend on when the files were
// actually written to everyone's drives.
ASSERT_EQ(components.size(), other_components.size());
for (size_t n = 0; n < components.size(); ++n) {
other_components[n].modified_time = components[n].modified_time;
EXPECT_NE(components, other_components);
}
}
TEST_F(GameletComponentTest, Serialization) {
std::vector<GameletComponent> components;
EXPECT_OK(GameletComponent::Get({valid_component_path_}, &components));
std::string args = GameletComponent::ToCommandLineArgs(components);
// FromCommandLineArgs() for a single string arg.
std::vector<GameletComponent> deserialized_components =
GameletComponent::FromCommandLineArgs(args);
EXPECT_EQ(components, deserialized_components);
// FromCommandLineArgs() for argc/argv.
std::vector<std::string> args_vec = absl::StrSplit(args, ' ');
int argc = static_cast<int>(args_vec.size());
std::vector<const char*> argv;
for (const std::string& arg : args_vec) argv.push_back(arg.c_str());
deserialized_components =
GameletComponent::FromCommandLineArgs(argc, argv.data());
EXPECT_EQ(components, deserialized_components);
}
} // namespace
} // namespace cdc_ft

55
common/grpc_status.h Normal file
View File

@@ -0,0 +1,55 @@
/*
* 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 COMMON_GRPC_STATUS_H_
#define COMMON_GRPC_STATUS_H_
#include "absl/status/status.h"
#include "grpcpp/grpcpp.h"
namespace cdc_ft {
//
// gRPC status conversion convenience methods.
//
inline grpc::Status ToGrpcStatus(const absl::Status& status) {
if (status.ok()) return grpc::Status::OK;
return grpc::Status(static_cast<grpc::StatusCode>(status.code()),
std::string(status.message()));
}
inline absl::Status ToAbslStatus(const grpc::Status& status) {
if (status.ok()) return absl::OkStatus();
return absl::Status(static_cast<absl::StatusCode>(status.error_code()),
std::string(status.error_message()));
}
#define RETURN_GRPC_IF_ERROR(expr) \
do { \
absl::Status __absl_status = (expr); \
if (!__absl_status.ok()) return ToGrpcStatus(__absl_status); \
} while (0)
#define RETURN_ABSL_IF_ERROR(expr) \
do { \
grpc::Status __grpc_status = (expr); \
if (!__grpc_status.ok()) return ToAbslStatus(__grpc_status); \
} while (0)
} // namespace cdc_ft
#endif // COMMON_GRPC_STATUS_H_

170
common/log.cc Normal file
View File

@@ -0,0 +1,170 @@
// 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 "log.h"
#include <cassert>
#include "common/platform.h"
#if PLATFORM_WINDOWS
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#endif
namespace cdc_ft {
namespace {
const char* GetLogLevelString(LogLevel level) {
switch (level) {
case LogLevel::kVerbose:
return "VERBOSE";
case LogLevel::kDebug:
return "DEBUG";
case LogLevel::kInfo:
return "INFO";
case LogLevel::kWarning:
return "WARNING";
case LogLevel::kError:
return "ERROR";
}
return "UnknownLogLevel";
}
} // namespace
// static
void Log::Initialize(std::unique_ptr<Log> log) {
assert(!instance_);
instance_ = log.release();
}
// static
void Log::Shutdown() {
assert(instance_);
delete instance_;
instance_ = nullptr;
}
// static
Log* Log::Instance() {
assert(instance_);
return instance_;
}
// static
Log* Log::MaybeNullInstance() { return instance_; }
Log* Log::instance_ = nullptr;
Log::~Log() = default;
// static
LogLevel Log::VerbosityToLogLevel(int verbosity) {
if (verbosity >= 4) {
return LogLevel::kVerbose;
}
if (verbosity >= 3) {
return LogLevel::kDebug;
}
if (verbosity >= 2) {
return LogLevel::kInfo;
}
return LogLevel::kWarning;
}
// static
void Log::DefaultWriteLogMessage(LogLevel level, const char* file, int line,
const char* message) {
// Only print warnings and above.
if (level < LogLevel::kWarning) return;
fprintf(stderr, "%-7s %s(%i): %s\n", GetLogLevelString(level), file, line,
message);
}
#if PLATFORM_WINDOWS
enum Colors {
kLightGray = 7,
kGray = 8,
kBlue = 9,
kCyan = 11,
kRed = 12,
kYellow = 14,
kWhite = 15
};
WORD GetConsoleColor(LogLevel level) {
switch (level) {
case LogLevel::kVerbose:
return kGray;
case LogLevel::kDebug:
return kCyan;
case LogLevel::kInfo:
return kWhite;
case LogLevel::kWarning:
return kYellow;
case LogLevel::kError:
return kRed;
}
return 15;
}
#endif
void ConsoleLog::WriteLogMessage(LogLevel level, const char* file, int line,
const char* func, const char* message) {
absl::MutexLock lock(&mutex_);
// Show leaner log messages in non-verbose mode.
bool show_file_func = GetLogLevel() <= LogLevel::kDebug;
FILE* stdfile = level >= LogLevel::kError ? stderr : stdout;
#if PLATFORM_WINDOWS
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTextAttribute(hConsole, GetConsoleColor(level));
if (show_file_func) {
fprintf(stdfile, "%s(%i): %s(): %s\n", file, line, func, message);
} else {
fprintf(stdfile, "%s\n", message);
}
SetConsoleTextAttribute(hConsole, kLightGray);
#else
if (show_file_func) {
fprintf(stdfile, "%-7s %s(%i): %s(): %s\n", GetLogLevelString(level), file,
line, func, message);
} else {
fprintf(stdfile, "%-7s %s\n", GetLogLevelString(level), message);
}
#endif
}
FileLog::FileLog(LogLevel log_level, const char* path) : Log(log_level) {
file_ = fopen(path, "wt");
if (!file_) fprintf(stderr, "Failed to open log file '%s'", path);
}
FileLog::~FileLog() {
if (file_) fclose(file_);
}
void FileLog::WriteLogMessage(LogLevel level, const char* file, int line,
const char* func, const char* message) {
if (!file_) return;
std::string timestamp = clock_.FormatNow("%Y-%m-%d %H:%M:%S.", true);
absl::MutexLock lock(&mutex_);
fprintf(file_, "%s %-7s %s(%i): %s(): %s\n", timestamp.c_str(),
GetLogLevelString(level), file, line, func, message);
fflush(file_);
}
} // namespace cdc_ft

144
common/log.h Normal file
View File

@@ -0,0 +1,144 @@
/*
* 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 COMMON_LOG_H_
#define COMMON_LOG_H_
#include <memory>
#include "absl/strings/str_format.h"
#include "absl/synchronization/mutex.h"
#include "common/clock.h"
namespace cdc_ft {
enum class LogLevel { kVerbose, kDebug, kInfo, kWarning, kError };
// Note: Bazel always uses forward slashes for paths.
#define __FILENAME__ \
(strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__)
#define LOG_VERBOSE(...) \
::cdc_ft::Log::SafePrintf(::cdc_ft::LogLevel::kVerbose, __FILENAME__, \
__LINE__, __func__, __VA_ARGS__);
#define LOG_DEBUG(...) \
::cdc_ft::Log::SafePrintf(::cdc_ft::LogLevel::kDebug, __FILENAME__, \
__LINE__, __func__, __VA_ARGS__);
#define LOG_INFO(...) \
::cdc_ft::Log::SafePrintf(::cdc_ft::LogLevel::kInfo, __FILENAME__, __LINE__, \
__func__, __VA_ARGS__);
#define LOG_WARNING(...) \
::cdc_ft::Log::SafePrintf(::cdc_ft::LogLevel::kWarning, __FILENAME__, \
__LINE__, __func__, __VA_ARGS__);
#define LOG_ERROR(...) \
::cdc_ft::Log::SafePrintf(::cdc_ft::LogLevel::kError, __FILENAME__, \
__LINE__, __func__, __VA_ARGS__);
#define LOG_LEVEL(level, ...) \
::cdc_ft::Log::SafePrintf(level, __FILENAME__, __LINE__, __func__, \
__VA_ARGS__);
class Log {
public:
// Initializes the Log singleton. Should be called in the beginning.
static void Initialize(std::unique_ptr<Log> log);
// Deletes the Log singleton. Must be called in the end.
static void Shutdown();
// Returns the global log instance.
static Log* Instance();
template <typename... Args>
static void SafePrintf(LogLevel level, const char* file, int line,
const char* func,
const absl::FormatSpec<Args...>& format,
Args... args) {
Log* inst = MaybeNullInstance();
if (inst) {
inst->Printf(level, file, line, func, format, args...);
} else {
DefaultWriteLogMessage(level, file, line,
absl::StrFormat(format, args...).c_str());
}
}
template <typename... Args>
void Printf(LogLevel level, const char* file, int line, const char* func,
const absl::FormatSpec<Args...>& format, Args... args) {
if (log_level_ <= level) {
WriteLogMessage(level, file, line, func,
absl::StrFormat(format, args...).c_str());
}
}
static LogLevel VerbosityToLogLevel(int verbosity);
void SetLogLevel(LogLevel log_level) { log_level_ = log_level; }
LogLevel GetLogLevel() const { return log_level_; }
virtual ~Log();
protected:
explicit Log(LogLevel log_level) : log_level_(log_level) {}
virtual void WriteLogMessage(LogLevel level, const char* file, int line,
const char* func, const char* message) = 0;
private:
// A default log writer implementation in case the log was not initialized.
static void DefaultWriteLogMessage(LogLevel level, const char* file, int line,
const char* message);
// Returns the global log instance.
static Log* MaybeNullInstance();
static Log* instance_;
LogLevel log_level_;
};
class ConsoleLog : public Log {
public:
explicit ConsoleLog(LogLevel log_level) : Log(log_level) {}
protected:
void WriteLogMessage(LogLevel level, const char* file, int line,
const char* func, const char* message) override
ABSL_LOCKS_EXCLUDED(mutex_);
private:
absl::Mutex mutex_;
};
class FileLog : public Log {
public:
FileLog(LogLevel log_level, const char* path);
~FileLog();
protected:
void WriteLogMessage(LogLevel level, const char* file, int line,
const char* func, const char* message) override
ABSL_LOCKS_EXCLUDED(mutex_);
private:
FILE* file_;
DefaultSystemClock clock_;
absl::Mutex mutex_;
};
} // namespace cdc_ft
#endif // COMMON_LOG_H_

134
common/log_test.cc Normal file
View File

@@ -0,0 +1,134 @@
// 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 "common/log.h"
#include "absl/strings/match.h"
#include "common/path.h"
#include "common/status_test_macros.h"
#include "gtest/gtest.h"
namespace cdc_ft {
namespace {
constexpr char kLogMessage[] = "Test log";
class TestLog : public Log {
public:
explicit TestLog(LogLevel log_level) : Log(log_level) {}
LogLevel LastLevel() const { return last_level_; }
const std::string& LastFunc() const { return last_func_; }
const std::string& LastMessage() const { return last_message_; }
protected:
void WriteLogMessage(LogLevel level, const char* file, int line,
const char* func, const char* message) override {
constexpr char expected_file[] = "log_test.cc";
// The full filename depends on the compiler invocation, so we just compare
// the basename.
EXPECT_TRUE(absl::StrContains(file, expected_file))
<< "File name '" << file << "' does not contain expected name '"
<< expected_file << "'.";
EXPECT_GT(line, 0);
last_level_ = level;
last_func_ = func;
last_message_ = message;
}
private:
LogLevel last_level_;
std::string last_func_;
std::string last_message_;
};
class LogTest : public ::testing::Test {
public:
void SetUp() override {
Log::Initialize(std::make_unique<TestLog>(LogLevel::kInfo));
}
void TearDown() override { Log::Shutdown(); }
protected:
TestLog* Log() const { return static_cast<TestLog*>(Log::Instance()); }
};
TEST_F(LogTest, LogMessage) {
LOG_INFO(kLogMessage);
EXPECT_EQ(kLogMessage, Log()->LastMessage());
}
TEST_F(LogTest, LogFormattedMessage) {
LOG_INFO("Test %i %s", 123, "message");
EXPECT_EQ("Test 123 message", Log()->LastMessage());
}
TEST_F(LogTest, IgnoresBelowLogLevel) {
EXPECT_EQ(Log()->GetLogLevel(), LogLevel::kInfo);
LOG_DEBUG(kLogMessage);
EXPECT_EQ("", Log()->LastMessage());
Log()->SetLogLevel(LogLevel::kDebug);
LOG_DEBUG(kLogMessage);
EXPECT_EQ(kLogMessage, Log()->LastMessage());
}
TEST_F(LogTest, VerbosityToLogLevel) {
EXPECT_EQ(Log::VerbosityToLogLevel(0), LogLevel::kWarning);
EXPECT_EQ(Log::VerbosityToLogLevel(1), LogLevel::kWarning);
EXPECT_EQ(Log::VerbosityToLogLevel(2), LogLevel::kInfo);
EXPECT_EQ(Log::VerbosityToLogLevel(3), LogLevel::kDebug);
EXPECT_EQ(Log::VerbosityToLogLevel(4), LogLevel::kVerbose);
EXPECT_EQ(Log::VerbosityToLogLevel(5), LogLevel::kVerbose);
}
TEST(FileLogTest, LogToFile) {
std::string tmp_dir = path::GetTempDir();
std::string log_path = path::Join(tmp_dir, "__log_unittest.log");
Log::Initialize(std::make_unique<FileLog>(LogLevel::kInfo, log_path.c_str()));
LOG_ERROR("Error");
LOG_INFO("Info");
LOG_DEBUG("Debug");
Log::Shutdown();
std::vector<std::string> lines;
ASSERT_OK(
path::ReadAllLines(log_path, &lines, path::ReadFlags::kRemoveEmpty));
ASSERT_EQ(lines.size(), 2);
EXPECT_TRUE(absl::StrContains(lines[0], "ERROR")) << lines[0];
EXPECT_TRUE(absl::StrContains(lines[0], "log_test.cc")) << lines[0];
EXPECT_TRUE(absl::StrContains(lines[0], "Error")) << lines[0];
EXPECT_TRUE(absl::StrContains(lines[1], "INFO")) << lines[1];
EXPECT_TRUE(absl::StrContains(lines[1], "log_test.cc")) << lines[1];
EXPECT_TRUE(absl::StrContains(lines[1], "Info")) << lines[1];
// Check date and time.
int yy, mo, da, hh, mm, ss, msec;
int scan_res = sscanf(lines[0].c_str(), "%04d-%02d-%02d %02d:%02d:%02d.%03d",
&yy, &mo, &da, &hh, &mm, &ss, &msec);
EXPECT_EQ(scan_res, 7) << lines[0];
}
TEST(NoLogTest, LogNotInitialized) {
// Using the log before initializing it should not trigger an assertion.
LOG_ERROR("Error");
LOG_INFO("Info");
LOG_DEBUG("Debug");
}
} // namespace
} // namespace cdc_ft

1190
common/path.cc Normal file

File diff suppressed because it is too large Load Diff

417
common/path.h Normal file
View File

@@ -0,0 +1,417 @@
/*
* 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 COMMON_PATH_H_
#define COMMON_PATH_H_
#include <cstdio>
#include <functional>
#include <string>
#include <vector>
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/string_view.h"
#include "common/platform.h"
#if PLATFORM_LINUX
#define feof_nolock feof_unlocked
#define fread_nolock fread_unlocked
#define fseek64 fseeko
#define fseek64_nolock fseeko
#define ftell64 ftello
#define fwrite_nolock fwrite_unlocked
#elif PLATFORM_WINDOWS
#define feof_nolock feof
#define fread_nolock _fread_nolock
#define fseek64 _fseeki64
#define fseek64_nolock _fseeki64_nolock
#define ftell64 _ftelli64
#define fwrite_nolock _fwrite_nolock
#endif
namespace cdc_ft {
class Buffer;
namespace path {
// Returns the native path separator character for this platform.
inline char PathSeparator() {
#ifdef PLATFORM_WINDOWS
return '\\';
#else
return '/';
#endif
}
// Returns the non-native path separator character for this platform.
inline char OtherPathSeparator() {
#ifdef PLATFORM_WINDOWS
return '/';
#else
return '\\';
#endif
}
// Closes the given FILE pointer once the object is deleted.
class FileCloser {
public:
explicit FileCloser(FILE* f) : fp_(f) {}
~FileCloser() {
if (fp_) fclose(fp_);
}
private:
FILE* fp_;
};
// All file paths are assumed to be UTF-8 encoded.
// Returns the |dir| that contains the current executable.
// The returned string does not have a trailing path separator.
absl::Status GetExeDir(std::string* dir);
// Returns a directory suitable for temporary files. The path is guaranteed to
// exist and to be a directory.
std::string GetTempDir();
#if PLATFORM_WINDOWS
enum class FolderId {
// E.g. C:\Users\jdoe\AppData\Roaming.
kRoamingAppData,
// E.g. C:\Program Files.
kProgramFiles
};
// Returns the Windows known folder path for the given |folder_id|.
absl::Status GetKnownFolderPath(FolderId folder_id, std::string* path);
// Expands environment path variables like %APPDATA%. Variables are matched
// case invariantly. Unknown environment variables are not changed.
absl::Status ExpandEnvironmentPathVariables(std::string* path);
#endif
// Returns the environment variable with given |name| in |value|.
// Returns a NotFound error and sets |value| to an empty string if the variable
// does not exist.
absl::Status GetEnv(const std::string& name, std::string* value);
// Sets the environment variable with given |name| to |value|. Only affects the
// current process, not system environment variables nor environment variables
// of other processes.
absl::Status SetEnv(const std::string& name, const std::string& value);
#if PLATFORM_WINDOWS
// Returns the part before the actual directory path without trailing path
// separator, i.e.
// - the drive letter "C:" for "C:\path\to\file" or
// - the network share "\\computer\share" for "\\computer\share\path\to\file".
// Returns an empty string if there is no such prefix (e.g. relative paths).
std::string GetDrivePrefix(const std::string& path);
#endif
// Gets the current working directory.
std::string GetCwd();
// Expands a relative path to an absolute path (relative to the current working
// directory). Also canonicalizes the path, removing any . and .. elements.
// Note that if the path does not exist or contains invalid characters, it may
// only be partially canonicalized, see
// https://en.cppreference.com/w/cpp/filesystem/canonical.
std::string GetFullPath(const std::string& path);
// Returns true if the given |path| is an absolute path.
bool IsAbsolute(const std::string& path);
// Returns the parent directory part of a |path|, without the trailing path
// separator. For instance, for "foo/bar", "foo/bar/", "foo\bar" or "foo\bar\",
// "foo" is returned.
// On Windows, if |path| only consists of a drive prefix, then the drive prefix
// is returned (see GetDrivePrefix()). If the path only has one component "foo",
// an empty string is returned.
std::string DirName(const std::string& path);
// Returns the last component of |path|. For instance, for "foo/bar",
// "foo/bar/", or "foo\bar", only "bar" is returned.
// If the path only has one component "bar", that component is returned.
std::string BaseName(const std::string& path);
// Joins |path| and |to_append|, adding a path separator if necessary.
std::string Join(absl::string_view path, absl::string_view to_append);
// Joins |path| and |to_append*|, adding a path separator if necessary.
std::string Join(absl::string_view path, absl::string_view to_append1,
absl::string_view to_append2);
// Joins |path| and |to_append*|, adding a path separator if necessary.
std::string Join(absl::string_view path, absl::string_view to_append1,
absl::string_view to_append2, absl::string_view to_append3);
// Joins |path| and |to_append|, adding a path separator if necessary, and
// stores the result in |dest|. |dest| must not overlap with |path| or
// |to_append|.
void Join(std::string* dest, absl::string_view path,
absl::string_view to_append);
// Joins |path| and |to_append|, adding a Unix path separator if necessary.
std::string JoinUnix(absl::string_view path, absl::string_view to_append);
// Appends |path| to |dest|, adding a path separator if necessary.
void Append(std::string* dest, absl::string_view to_append);
// Appends |path| to |dest|, adding a Unix path separator if necessary.
void AppendUnix(std::string* dest, absl::string_view to_append);
// Returns true if |path| ends with '\' or '//'.
bool EndsWithPathSeparator(const std::string& path);
// Adds a path separator at the end if there is none yet.
void EnsureEndsWithPathSeparator(std::string* path);
// Removes all path separators at the end if there are any.
void EnsureDoesNotEndWithPathSeparator(std::string* path);
// Converts path separators to the current platform's version, e.g. \ to /
// on Linux.
void FixPathSeparators(std::string* path);
// Converts a Windows path having backslashes as directory separators to a Unix
// path with forward slashes. A leading Windows drive letter like "C:" will be
// unchanged. For example: "C:\foo\bar" is converted to "C:/foo/bar".
std::string ToUnix(std::string path);
// Converts a path (eg, Windows or Unix path) to current system path.
// Substitutes forward slashes with backward slashes for Windows,
// substitutes backward slashes with forward slashes for Unix.
// A leading Windows drive letter like "C:" will be unchanged.
std::string ToNative(std::string path);
// Opens a file for the given |mode|.
absl::StatusOr<FILE*> OpenFile(const std::string& path, const char* mode);
using SearchHandler = std::function<absl::Status(
const std::string& dir, const std::string& filename, int64_t modified_time,
uint64_t size, bool is_dir)>;
// Searches all files and directories for which the path matches |pattern|.
//
// On Windows, if |pattern| is a directory path, finds that directory,
// traverses it, and finds all files and subdirectories in that directory.
// If |pattern| is a file path, finds that file. If |pattern| is
// a directory plus a file name pattern, e.g. "C:\files\*.t?t", returns all
// files and directories in that directory for which the path names match
// the pattern. Directory wildcards are not supported. A directory ending with
// "\" is treated like "\*", so it will not find the base directory, only
// subdirectories.
//
// On Linux, |pattern| must be a directory name and the function returns all
// files and directories in that directory. Glob-style patterns or file name
// matchings are not supported.
//
// If |recursive| is true, recurses into subdirectories. Does best effort,
// errors accessing directories does not stop the search, but prints out
// warnings (for access denied) or errors.
//
// Calls |handler| for every file and directory found. If the |handler| returns
// an error, the search is interrupted and the method returns that error.
// |modified_time| passed to |handler| is UTC time.
absl::Status SearchFiles(const std::string& pattern, bool recursive,
SearchHandler handler);
using StreamReadFileHandler =
std::function<absl::Status(const void* data, size_t data_size)>;
// Reads |file| in chunks of size |buffer_size| and calls |handler|. On EOF,
// calls handler(nullptr, 0). If |handler| returns an error, reading stops
// and the function returns that error. Also returns an error if a read fails.
absl::Status StreamReadFileContents(FILE* file, size_t buffer_size,
StreamReadFileHandler handler);
// Same as above, but uses the pre-allocated memory in |buffer|.
absl::Status StreamReadFileContents(FILE* file, Buffer* buffer,
StreamReadFileHandler handler);
using StreamWriteFileHandler =
std::function<absl::Status(const void** data, size_t* data_size)>;
// Calls |handler| and writes the data it returns to |file|. If |handler| sets
// (data, size) to (nullptr, 0) and returns ok, the function returns ok. If
// |handler| returns an error, the function returns that error. Also returns
// an error if a write fails.
absl::Status StreamWriteFileContents(FILE* file,
StreamWriteFileHandler handler);
// Reads the contents of the file at |path| to |data|.
absl::Status ReadFile(const std::string& path, Buffer* data);
// Reads at most |len| bytes starting at |offset| of the file
// at |path| into |data|. Returns the number of read bytes.
absl::StatusOr<size_t> ReadFile(const std::string& path, void* data,
size_t offset, size_t len);
// Reads the contents of the file at |path| and returns it as string.
absl::StatusOr<std::string> ReadFile(const std::string& path);
enum class ReadFlags {
kNone = 0, // Read all lines.
kTrimWhitespace = 1 << 0, // Trim whitespace from each line.
kRemoveEmpty = 1 << 1 // Remove empty lines (after optional trim).
};
// Reads the contents of the file at |path| line-by-line to |lines|.
absl::Status ReadAllLines(const std::string& path,
std::vector<std::string>* lines, ReadFlags flags);
// Writes |data| of |len| bytes to the file at |path|. If the file does not
// exist, it is created. Otherwise, its content is discarded.
absl::Status WriteFile(const std::string& path, const void* data, size_t len);
// Writes |data| to the file at |path|.
absl::Status WriteFile(const std::string& path, const Buffer& data);
// Writes |data| to the file at |path|.
absl::Status WriteFile(const std::string& path, const std::string& data);
// Creates a symlink at |link_path| pointing to |target|.
// |is_dir| should be true if |target| is a directory, otherwise false.
absl::Status CreateSymlink(const std::string& target,
const std::string& link_path, bool is_dir);
// Retrieves a symlink target at |link_path|.
absl::StatusOr<std::string> GetSymlinkTarget(const std::string& link_path);
// Checks the directory at |path| exists.
bool DirExists(const std::string& path);
// Checks the file or directory at |path| exists.
bool FileExists(const std::string& path);
// Checks the file or directory at |path| exists.
bool Exists(const std::string& path);
// Creates a directory for |path|.
// If the path already exists, the call is a no-op and returns success.
// Only creates the top-level directory. The call fails if intermediate
// directories are missing.
absl::Status CreateDir(const std::string& path);
// Creates a directory for |path|.
// If the path already exists, the call is a no-op and returns success.
// Also creates missing intermediate directories.
absl::Status CreateDirRec(const std::string& path);
// Renames/moves from |from_path| to |to_path|. Note that Windows fails when
// |to_path| exists, while Linux atomically replaces the |to_path| with
// |from_path|.
absl::Status RenameFile(const std::string& from_path,
const std::string& to_path);
// Copies from |from_path| to |to_path|.
// Also creates missing intermediate directories.
// Note that on Windows it's impossible to move or replace items between drives.
// In that case this method should be used.
absl::Status CopyFileRec(const std::string& from_path,
const std::string& to_path);
// Deletes the file at |path|. Returns OK if the file was deleted, or if
// |path| does not exist. On Windows, this requires write permissions to the
// file, while on Linux, it requires write permissions to the directory that
// contains the file.
absl::Status RemoveFile(const std::string& path);
// Deletes the file at |path| and moves the file at |replacement_path| to
// |old_path|.
absl::Status ReplaceFile(const std::string& path,
const std::string& replacement_path);
// Mode bits for GetStats. It's a bit unclear how these work on Windows.
// Apparently, the group/other flags are mirrored from the user flags.
enum Mode {
MODE_IFMT = 0170000, // File type bits
MODE_IFREG = 0100000, // Regular file
MODE_IFDIR = 0040000, // Directory
MODE_IFCHR = 0020000, // Character-oriented device file
MODE_IFIFO = 0010000, // FIFO or Pipe
MODE_ISUID = 0004000, // Set-user-ID on execute bit
MODE_ISGID = 0002000, // Set-group-ID on execute bit
MODE_ISVTX = 0001000, // Sticky bit
MODE_IRWXU = 0000700, // User rwx
MODE_IRUSR = 0000400, // User r
MODE_IWUSR = 0000200, // User w
MODE_IXUSR = 0000100, // User x
MODE_IRWXG = 0000070, // Group rwx
MODE_IRGRP = 0000040, // Group r
MODE_IWGRP = 0000020, // Group w
MODE_IXGRP = 0000010, // Group x
MODE_IRWXO = 0000007, // Other rwx
MODE_IROTH = 0000004, // Other r
MODE_IWOTH = 0000002, // Other w
MODE_IXOTH = 0000001, // Other x
};
struct Stats {
uint16_t mode = 0; // Mode enum
int64_t modified_time = 0; // Unix epoch (local time)
uint64_t size = 0;
};
// Returns stats for |path|. |modified_time| in
// the returned struct is local time. In other words
// it is the number of ticks from start of epoch by local time.
// TODO: Use consistent timestamps.
absl::Status GetStats(const std::string& path, Stats* stats);
// Returns the file size of the given file.
absl::Status FileSize(const std::string& path, uint64_t* size);
// Changes the mode bits of the file at |path| to |mode|.
absl::Status ChangeMode(const std::string& path, uint16_t mode);
// Removes |path| recursively.
absl::Status RemoveDirRec(const std::string& path);
// Reads the modification time of |path| and writes it to the given Unix
// epoch |timestamp| in UTC. |path| can point to a file or a directory.
absl::Status GetFileTime(const std::string& path, time_t* timestamp);
// Updates both the access and modification time of |path| to the given Unix
// epoch |timestamp| in UTC.
absl::Status SetFileTime(const std::string& path, time_t timestamp);
// Compares |path1| and |path2| and returns true if they are equal.
// Converts both paths to canonical form before coparison.
bool AreEqual(std::string path1, std::string path2);
} // namespace path
inline path::ReadFlags operator|(path::ReadFlags a, path::ReadFlags b) {
using T = std::underlying_type_t<path::ReadFlags>;
return static_cast<path::ReadFlags>(static_cast<T>(a) | static_cast<T>(b));
}
inline path::ReadFlags operator&(path::ReadFlags a, path::ReadFlags b) {
using T = std::underlying_type_t<path::ReadFlags>;
return static_cast<path::ReadFlags>(static_cast<T>(a) & static_cast<T>(b));
}
} // namespace cdc_ft
#endif // COMMON_PATH_H_

80
common/path_filter.cc Normal file
View File

@@ -0,0 +1,80 @@
// 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 "common/path_filter.h"
namespace cdc_ft {
namespace internal {
bool IsMatch(const char* pattern, const char* path, size_t path_len) {
for (/*empty*/; *pattern != '\0'; ++pattern) {
switch (*pattern) {
case '?':
if (*path == '\0') {
return false;
}
++path;
break;
case '*': {
if (pattern[1] == '\0') {
return true;
}
for (size_t n = 0; n < path_len; n++) {
if (IsMatch(pattern + 1, path + n, path_len - n)) {
return true;
}
}
return false;
}
default:
if (*path != *pattern) {
return false;
}
++path;
}
}
return *path == '\0';
}
bool IsMatch(const std::string& pattern, const std::string& path) {
return IsMatch(pattern.c_str(), path.c_str(), path.size());
}
} // namespace internal
PathFilter::PathFilter() = default;
PathFilter::~PathFilter() = default;
bool PathFilter::IsEmpty() const { return rules_.empty(); }
void PathFilter::AddRule(Rule::Type type, std::string pattern) {
rules_.emplace_back(type, std::move(pattern));
}
bool PathFilter::IsMatch(const std::string& path) const {
for (const Rule& rule : rules_) {
if (internal::IsMatch(rule.pattern.c_str(), path.c_str(), path.size())) {
return rule.type == Rule::Type::kInclude;
}
}
return true;
}
PathFilter::Rule::Rule(Type type, std::string pattern)
: type(type), pattern(std::move(pattern)) {}
} // namespace cdc_ft

72
common/path_filter.h Normal file
View File

@@ -0,0 +1,72 @@
/*
* 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 COMMON_PATH_FILTER_H_
#define COMMON_PATH_FILTER_H_
#include <string>
#include <vector>
namespace cdc_ft {
namespace internal {
// Returns true iff |path| matches |pattern|. Exposed for testing purposes.
bool IsMatch(const std::string& pattern, const std::string& path);
} // namespace internal
// Applies include and exclude filter rules to a path.
class PathFilter {
public:
struct Rule {
enum class Type {
kInclude,
kExclude,
};
Type type;
std::string pattern;
Rule(Type type, std::string pattern);
};
PathFilter();
~PathFilter();
// Returns true if no rule has been added.
bool IsEmpty() const;
// Adds a rule to the set of filter rules. The |type| determines whether paths
// that match |pattern| should be filtered in or out, see IsMatch(). |pattern|
// is a Windows style file path pattern and supports * and ? wildcards. Does
// not support general glob-style ** or [a-z] patterns.
void AddRule(Rule::Type type, std::string pattern);
// Returns all rules added.
const std::vector<Rule>& GetRules() const { return rules_; }
// Applies filter rules in the order they have been passed to AddRule().
// Returns true if the first matching rule is of type |kInclude| or no
// matching rule is found. Returns false if the first matching rule is
// of type |kExclude|.
bool IsMatch(const std::string& path) const;
private:
std::vector<Rule> rules_;
};
} // namespace cdc_ft
#endif // COMMON_PATH_FILTER_H_

130
common/path_filter_test.cc Normal file
View File

@@ -0,0 +1,130 @@
// 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 "common/path_filter.h"
#include "gtest/gtest.h"
namespace cdc_ft {
namespace {
class PathFilterTest : public ::testing::Test {};
TEST_F(PathFilterTest, IsMatchFn_NoWildcard) {
EXPECT_TRUE(internal::IsMatch("", ""));
EXPECT_TRUE(internal::IsMatch("a", "a"));
EXPECT_FALSE(internal::IsMatch("a", "b"));
EXPECT_TRUE(internal::IsMatch("abc", "abc"));
EXPECT_FALSE(internal::IsMatch("abc", "abd"));
}
TEST_F(PathFilterTest, IsMatchFn_Questionmark) {
EXPECT_FALSE(internal::IsMatch("?", ""));
EXPECT_TRUE(internal::IsMatch("?", "a"));
EXPECT_FALSE(internal::IsMatch("?", "ab"));
EXPECT_TRUE(internal::IsMatch("??", "ab"));
EXPECT_FALSE(internal::IsMatch("??", "a"));
}
TEST_F(PathFilterTest, IsMatchFn_Asterisk) {
EXPECT_TRUE(internal::IsMatch("*", ""));
EXPECT_TRUE(internal::IsMatch("*", "a"));
EXPECT_TRUE(internal::IsMatch("*", "ab"));
EXPECT_TRUE(internal::IsMatch("*", "abc"));
EXPECT_TRUE(internal::IsMatch("a**", "abc"));
EXPECT_FALSE(internal::IsMatch("b**", "abc"));
EXPECT_FALSE(internal::IsMatch("c**", "abc"));
EXPECT_TRUE(internal::IsMatch("*a*", "abc"));
EXPECT_TRUE(internal::IsMatch("*b*", "abc"));
EXPECT_TRUE(internal::IsMatch("*c*", "abc"));
EXPECT_FALSE(internal::IsMatch("**a", "abc"));
EXPECT_FALSE(internal::IsMatch("**b", "abc"));
EXPECT_TRUE(internal::IsMatch("**c", "abc"));
}
TEST_F(PathFilterTest, IsMatchFn_CommonCases) {
constexpr char p1[] = "*.txt";
EXPECT_TRUE(internal::IsMatch(p1, "file1.txt"));
EXPECT_FALSE(internal::IsMatch(p1, "file2.dat"));
constexpr char p2[] = "dir1\\dir2\\*.txt";
EXPECT_TRUE(internal::IsMatch(p2, "dir1\\dir2\\file1.txt"));
EXPECT_FALSE(internal::IsMatch(p2, "dir1\\dir2\\file2.dat"));
EXPECT_FALSE(internal::IsMatch(p2, "dir1\\file3.txt"));
constexpr char p3[] = "dir1\\dir?\\*.txt";
EXPECT_TRUE(internal::IsMatch(p3, "dir1\\dir2\\file1.txt"));
EXPECT_TRUE(internal::IsMatch(p3, "dir1\\dir3\\file2.txt"));
EXPECT_FALSE(internal::IsMatch(p3, "dir1\\file3.txt"));
constexpr char p4[] = "dir1\\*\\*.txt";
EXPECT_FALSE(internal::IsMatch(p4, "dir1\\file1.txt"));
EXPECT_TRUE(internal::IsMatch(p4, "dir1\\dir2\\file1.txt"));
EXPECT_TRUE(internal::IsMatch(p4, "dir1\\dir3\\file2.txt"));
// Note: This is different from glob behavior!
EXPECT_TRUE(internal::IsMatch(p4, "dir1\\dir4\\dir5\\file3.txt"));
EXPECT_TRUE(internal::IsMatch(p4, "dir1\\\\file2.txt"));
constexpr char p5[] = "dir1\\*\\dir3\\*\\*.txt";
EXPECT_FALSE(internal::IsMatch(p5, "dir1\\dir2\\file1.txt"));
EXPECT_FALSE(internal::IsMatch(p5, "dir1\\dir3\\file2.txt"));
EXPECT_TRUE(internal::IsMatch(p5, "dir1\\dir2\\dir3\\dir4\\file3.txt"));
}
TEST_F(PathFilterTest, IsMatch_NoFilter) {
PathFilter path_filter;
EXPECT_TRUE(path_filter.IsMatch(""));
EXPECT_TRUE(path_filter.IsMatch("a"));
EXPECT_TRUE(path_filter.IsMatch("b\\c"));
}
TEST_F(PathFilterTest, IsMatch_IncludeFilter) {
PathFilter path_filter;
path_filter.AddRule(PathFilter::Rule::Type::kInclude, "a");
EXPECT_TRUE(path_filter.IsMatch(""));
EXPECT_TRUE(path_filter.IsMatch("a"));
EXPECT_TRUE(path_filter.IsMatch("b\\c"));
}
TEST_F(PathFilterTest, IsMatch_ExcludeFilter) {
PathFilter path_filter;
path_filter.AddRule(PathFilter::Rule::Type::kExclude, "a");
EXPECT_TRUE(path_filter.IsMatch(""));
EXPECT_FALSE(path_filter.IsMatch("a"));
EXPECT_TRUE(path_filter.IsMatch("b\\c"));
}
TEST_F(PathFilterTest, IsMatch_IncludeThenExclude) {
PathFilter path_filter;
path_filter.AddRule(PathFilter::Rule::Type::kInclude, "file2.txt");
path_filter.AddRule(PathFilter::Rule::Type::kExclude, "*.txt");
EXPECT_FALSE(path_filter.IsMatch("file1.txt"));
EXPECT_TRUE(path_filter.IsMatch("file2.txt"));
EXPECT_TRUE(path_filter.IsMatch("file3.dat"));
}
TEST_F(PathFilterTest, IsMatch_ExcludeThenInclude) {
PathFilter path_filter;
path_filter.AddRule(PathFilter::Rule::Type::kExclude, "*.txt");
path_filter.AddRule(PathFilter::Rule::Type::kInclude, "file2.txt");
EXPECT_FALSE(path_filter.IsMatch("file1.txt"));
EXPECT_FALSE(path_filter.IsMatch("file2.txt"));
EXPECT_TRUE(path_filter.IsMatch("file3.dat"));
}
} // namespace
} // namespace cdc_ft

1521
common/path_test.cc Normal file

File diff suppressed because it is too large Load Diff

30
common/platform.h Normal file
View File

@@ -0,0 +1,30 @@
/*
* 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 COMMON_PLATFORM_H_
#define COMMON_PLATFORM_H_
#undef PLATFORM_LINUX
#undef PLATFORM_WINDOWS
#ifdef __linux__
#define PLATFORM_LINUX 1
#define PLATFORM_NAME "Linux"
#elif _WIN32
#define PLATFORM_WINDOWS 1
#define PLATFORM_NAME "Windows"
#endif
#endif // COMMON_PLATFORM_H_

113
common/port_manager.h Normal file
View File

@@ -0,0 +1,113 @@
/*
* 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 COMMON_PORT_MANAGER_H_
#define COMMON_PORT_MANAGER_H_
#include <absl/status/statusor.h>
#include <memory>
#include <string>
#include <unordered_set>
#include "common/clock.h"
namespace cdc_ft {
class ProcessFactory;
class RemoteUtil;
class SharedMemory;
// Class for reserving ports globally. Use if there can be multiple processes
// of the same type that might request ports at the same time, e.g. multiple
// cdc_rsync.exe processes running concurrently.
class PortManager {
public:
// |unique_name| is a globally unique name used for shared memory to
// synchronize port reservation. The range of possible ports managed by this
// instance is [|first_port|, |last_port|]. |process_factory| is a valid
// pointer to a ProcessFactory instance to run processes locally.
// |remote_util| is a valid pointer to a RemoteUtil instance to run processes
// remotely.
PortManager(std::string unique_name, int first_port, int last_port,
ProcessFactory* process_factory, RemoteUtil* remote_util,
SystemClock* system_clock = DefaultSystemClock::GetInstance(),
SteadyClock* steady_clock = DefaultSteadyClock::GetInstance());
~PortManager();
// Reserves a port in the range passed to the constructor. The port is
// released automatically upon destruction if ReleasePort() is not called
// explicitly.
// |timeout_sec| is the timeout for finding available ports on the gamelet
// instance. Returns a DeadlineExceeded error if the timeout is exceeded.
// Returns a ResourceExhausted error if no ports are available.
absl::StatusOr<int> ReservePort(int timeout_sec);
// Releases a reserved port.
absl::Status ReleasePort(int port);
//
// Lower-level interface for finding available ports directly.
//
// Finds available ports in the range [first_port, last_port] for port
// forwarding on the local workstation.
// |ip| is the IP address to filter by.
// |process_factory| is used to create a netstat process.
// |forward_output_to_log| determines whether the stderr of netstat is
// forwarded to the logs. Returns ResourceExhaustedError if no port is
// available.
static absl::StatusOr<std::unordered_set<int>> FindAvailableLocalPorts(
int first_port, int last_port, const char* ip,
ProcessFactory* process_factory, bool forward_output_to_log);
// Finds available ports in the range [first_port, last_port] for port
// forwarding on the instance.
// |ip| is the IP address to filter by.
// |process_factory| is used to create a netstat process.
// |remote_util| is used to connect to the instance.
// |timeout_sec| is the connection timeout in seconds.
// |forward_output_to_log| determines whether the stderr of netstat is
// forwarded to the logs. Returns a DeadlineExceeded error if the timeout is
// exceeded. Returns ResourceExhaustedError if no port is available.
static absl::StatusOr<std::unordered_set<int>> FindAvailableRemotePorts(
int first_port, int last_port, const char* ip,
ProcessFactory* process_factory, RemoteUtil* remote_util, int timeout_sec,
bool forward_output_to_log,
SteadyClock* steady_clock = DefaultSteadyClock::GetInstance());
private:
// Returns a list of available ports in the range [|first_port|, |last_port|]
// from the given |netstat_output|. |ip| is the IP address to look for, e.g.
// "127.0.0.1".
// Returns ResourceExhaustedError if no port is available.
static absl::StatusOr<std::unordered_set<int>> FindAvailablePorts(
int first_port, int last_port, const std::string& netstat_output,
const char* ip);
int first_port_;
int last_port_;
ProcessFactory* process_factory_;
RemoteUtil* remote_util_;
SystemClock* system_clock_;
SteadyClock* steady_clock_;
std::unique_ptr<SharedMemory> shared_mem_;
std::unordered_set<int> reserved_ports_;
};
} // namespace cdc_ft
#endif // COMMON_PORT_MANAGER_H_

256
common/port_manager_test.cc Normal file
View File

@@ -0,0 +1,256 @@
// 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 "common/port_manager.h"
#include "absl/strings/match.h"
#include "common/log.h"
#include "common/remote_util.h"
#include "common/status_test_macros.h"
#include "common/stub_process.h"
#include "common/testing_clock.h"
#include "gtest/gtest.h"
namespace cdc_ft {
namespace {
constexpr int kGameletPort = 12345;
constexpr char kGameletIp[] = "1.2.3.4";
constexpr char kGuid[] = "f77bcdfe-368c-4c45-9f01-230c5e7e2132";
constexpr int kFirstPort = 44450;
constexpr int kLastPort = 44459;
constexpr int kNumPorts = kLastPort - kFirstPort + 1;
constexpr int kTimeoutSec = 1;
constexpr char kLocalNetstat[] = "netstat -a -n -p tcp";
constexpr char kRemoteNetstat[] = "netstat --numeric --listening --tcp";
constexpr char kLocalNetstatOutFmt[] =
"TCP 127.0.0.1:50000 127.0.0.1:%i ESTABLISHED";
constexpr char kRemoteNetstatOutFmt[] =
"tcp 0 0 0.0.0.0:%i 0.0.0.0:* LISTEN";
class PortManagerTest : public ::testing::Test {
public:
PortManagerTest()
: remote_util_(/*verbosity=*/0, /*quiet=*/false, &process_factory_,
/*forward_output_to_log=*/true),
port_manager_(kGuid, kFirstPort, kLastPort, &process_factory_,
&remote_util_, &system_clock_, &steady_clock_) {}
void SetUp() override {
Log::Initialize(std::make_unique<ConsoleLog>(LogLevel::kInfo));
remote_util_.SetIpAndPort(kGameletIp, kGameletPort);
}
void TearDown() override { Log::Shutdown(); }
protected:
StubProcessFactory process_factory_;
TestingSystemClock system_clock_;
TestingSteadyClock steady_clock_;
RemoteUtil remote_util_;
PortManager port_manager_;
};
TEST_F(PortManagerTest, ReservePortSuccess) {
process_factory_.SetProcessOutput(kLocalNetstat, "", "", 0);
process_factory_.SetProcessOutput(kRemoteNetstat, "", "", 0);
absl::StatusOr<int> port = port_manager_.ReservePort(kTimeoutSec);
ASSERT_OK(port);
EXPECT_EQ(*port, kFirstPort);
}
TEST_F(PortManagerTest, ReservePortAllLocalPortsTaken) {
std::string local_netstat_out = "";
for (int port = kFirstPort; port <= kLastPort; ++port) {
local_netstat_out += absl::StrFormat(kLocalNetstatOutFmt, port);
}
process_factory_.SetProcessOutput(kLocalNetstat, local_netstat_out, "", 0);
process_factory_.SetProcessOutput(kRemoteNetstat, "", "", 0);
absl::StatusOr<int> port = port_manager_.ReservePort(kTimeoutSec);
EXPECT_TRUE(absl::IsResourceExhausted(port.status()));
EXPECT_TRUE(
absl::StrContains(port.status().message(), "No port available in range"));
}
TEST_F(PortManagerTest, ReservePortAllRemotePortsTaken) {
std::string remote_netstat_out = "";
for (int port = kFirstPort; port <= kLastPort; ++port) {
remote_netstat_out += absl::StrFormat(kRemoteNetstatOutFmt, port);
}
process_factory_.SetProcessOutput(kLocalNetstat, "", "", 0);
process_factory_.SetProcessOutput(kRemoteNetstat, remote_netstat_out, "", 0);
absl::StatusOr<int> port = port_manager_.ReservePort(kTimeoutSec);
EXPECT_TRUE(absl::IsResourceExhausted(port.status()));
EXPECT_TRUE(
absl::StrContains(port.status().message(), "No port available in range"));
}
TEST_F(PortManagerTest, ReservePortLocalNetstatFails) {
process_factory_.SetProcessOutput(kLocalNetstat, "", "", 1);
process_factory_.SetProcessOutput(kRemoteNetstat, "", "", 0);
absl::StatusOr<int> port = port_manager_.ReservePort(kTimeoutSec);
EXPECT_NOT_OK(port);
EXPECT_TRUE(
absl::StrContains(port.status().message(),
"Failed to find available ports on workstation"));
}
TEST_F(PortManagerTest, ReservePortRemoteNetstatFails) {
process_factory_.SetProcessOutput(kLocalNetstat, "", "", 0);
process_factory_.SetProcessOutput(kRemoteNetstat, "", "", 1);
absl::StatusOr<int> port = port_manager_.ReservePort(kTimeoutSec);
EXPECT_NOT_OK(port);
EXPECT_TRUE(absl::StrContains(port.status().message(),
"Failed to find available ports on instance"));
}
TEST_F(PortManagerTest, ReservePortRemoteNetstatTimesOut) {
process_factory_.SetProcessOutput(kLocalNetstat, "", "", 0);
process_factory_.SetProcessNeverExits(kRemoteNetstat);
steady_clock_.AutoAdvance(kTimeoutSec * 2 * 1000);
absl::StatusOr<int> port = port_manager_.ReservePort(kTimeoutSec);
EXPECT_NOT_OK(port);
EXPECT_TRUE(absl::IsDeadlineExceeded(port.status()));
EXPECT_TRUE(absl::StrContains(port.status().message(),
"Timeout while running netstat"));
}
TEST_F(PortManagerTest, ReservePortMultipleInstances) {
process_factory_.SetProcessOutput(kLocalNetstat, "", "", 0);
process_factory_.SetProcessOutput(kRemoteNetstat, "", "", 0);
PortManager port_manager2(kGuid, kFirstPort, kLastPort, &process_factory_,
&remote_util_);
// Port managers use shared memory, so different instances know about each
// other. This would even work if |port_manager_| and |port_manager2| belonged
// to different processes, but we don't test that here.
EXPECT_EQ(*port_manager_.ReservePort(kTimeoutSec), kFirstPort + 0);
EXPECT_EQ(*port_manager2.ReservePort(kTimeoutSec), kFirstPort + 1);
EXPECT_EQ(*port_manager_.ReservePort(kTimeoutSec), kFirstPort + 2);
EXPECT_EQ(*port_manager2.ReservePort(kTimeoutSec), kFirstPort + 3);
}
TEST_F(PortManagerTest, ReservePortReusesPortsInLRUOrder) {
process_factory_.SetProcessOutput(kLocalNetstat, "", "", 0);
process_factory_.SetProcessOutput(kRemoteNetstat, "", "", 0);
for (int n = 0; n < kNumPorts * 2; ++n) {
EXPECT_EQ(*port_manager_.ReservePort(kTimeoutSec),
kFirstPort + n % kNumPorts);
system_clock_.Advance(1000);
}
}
TEST_F(PortManagerTest, ReleasePort) {
process_factory_.SetProcessOutput(kLocalNetstat, "", "", 0);
process_factory_.SetProcessOutput(kRemoteNetstat, "", "", 0);
absl::StatusOr<int> port = port_manager_.ReservePort(kTimeoutSec);
EXPECT_EQ(*port, kFirstPort);
EXPECT_OK(port_manager_.ReleasePort(*port));
port = port_manager_.ReservePort(kTimeoutSec);
EXPECT_EQ(*port, kFirstPort);
}
TEST_F(PortManagerTest, ReleasePortOnDestruction) {
process_factory_.SetProcessOutput(kLocalNetstat, "", "", 0);
process_factory_.SetProcessOutput(kRemoteNetstat, "", "", 0);
auto port_manager2 = std::make_unique<PortManager>(
kGuid, kFirstPort, kLastPort, &process_factory_, &remote_util_);
EXPECT_EQ(*port_manager2->ReservePort(kTimeoutSec), kFirstPort + 0);
EXPECT_EQ(*port_manager_.ReservePort(kTimeoutSec), kFirstPort + 1);
port_manager2.reset();
EXPECT_EQ(*port_manager_.ReservePort(kTimeoutSec), kFirstPort + 0);
}
TEST_F(PortManagerTest, FindAvailableLocalPortsSuccess) {
// First port is taken
std::string local_netstat_out =
absl::StrFormat(kLocalNetstatOutFmt, kFirstPort);
process_factory_.SetProcessOutput(kLocalNetstat, local_netstat_out, "", 0);
absl::StatusOr<std::unordered_set<int>> ports =
PortManager::FindAvailableLocalPorts(kFirstPort, kLastPort, "127.0.0.1",
&process_factory_, true);
ASSERT_OK(ports);
EXPECT_EQ(ports->size(), kNumPorts - 1);
for (int port = kFirstPort + 1; port <= kLastPort; ++port) {
EXPECT_TRUE(ports->find(port) != ports->end());
}
}
TEST_F(PortManagerTest, FindAvailableLocalPortsFailsNoPorts) {
// All ports taken
std::string local_netstat_out = "";
for (int port = kFirstPort; port <= kLastPort; ++port) {
local_netstat_out += absl::StrFormat(kLocalNetstatOutFmt, port);
}
process_factory_.SetProcessOutput(kLocalNetstat, local_netstat_out, "", 0);
absl::StatusOr<std::unordered_set<int>> ports =
PortManager::FindAvailableLocalPorts(kFirstPort, kLastPort, "127.0.0.1",
&process_factory_, true);
EXPECT_TRUE(absl::IsResourceExhausted(ports.status()));
EXPECT_TRUE(absl::StrContains(ports.status().message(),
"No port available in range"));
}
TEST_F(PortManagerTest, FindAvailableRemotePortsSuccess) {
// First port is taken
std::string remote_netstat_out =
absl::StrFormat(kRemoteNetstatOutFmt, kFirstPort);
process_factory_.SetProcessOutput(kRemoteNetstat, remote_netstat_out, "", 0);
absl::StatusOr<std::unordered_set<int>> ports =
PortManager::FindAvailableRemotePorts(kFirstPort, kLastPort, "0.0.0.0",
&process_factory_, &remote_util_,
kTimeoutSec, true);
ASSERT_OK(ports);
EXPECT_EQ(ports->size(), kNumPorts - 1);
for (int port = kFirstPort + 1; port <= kLastPort; ++port) {
EXPECT_TRUE(ports->find(port) != ports->end());
}
}
TEST_F(PortManagerTest, FindAvailableRemotePortsFailsNoPorts) {
// All ports taken
std::string remote_netstat_out = "";
for (int port = kFirstPort; port <= kLastPort; ++port) {
remote_netstat_out += absl::StrFormat(kRemoteNetstatOutFmt, port);
}
process_factory_.SetProcessOutput(kRemoteNetstat, remote_netstat_out, "", 0);
absl::StatusOr<std::unordered_set<int>> ports =
PortManager::FindAvailableRemotePorts(kFirstPort, kLastPort, "0.0.0.0",
&process_factory_, &remote_util_,
kTimeoutSec, true);
EXPECT_TRUE(absl::IsResourceExhausted(ports.status()));
EXPECT_TRUE(absl::StrContains(ports.status().message(),
"No port available in range"));
}
} // namespace
} // namespace cdc_ft

302
common/port_manager_win.cc Normal file
View File

@@ -0,0 +1,302 @@
// 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 "common/port_manager.h"
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <map>
#include "absl/strings/str_split.h"
#include "common/log.h"
#include "common/process.h"
#include "common/remote_util.h"
#include "common/status.h"
#include "common/status_macros.h"
#include "common/stopwatch.h"
#include "common/util.h"
namespace cdc_ft {
class SharedMemory {
public:
// Creates a new shared memory instance with given |name| and |size| in bytes.
// Different instances with matching names reference the same piece of memory,
// even if they belong to different processes. If shared memory with the given
// |name| already exists, the existing memory is referenced. Otherwise, a new
// piece of memory is allocated and zero-initialized.
SharedMemory(std::string name, size_t size)
: name_(std::move(name)), size_(size) {}
absl::StatusOr<void*> Get() {
// Already initialized?
if (shared_mem_) return shared_mem_;
assert(!map_file_handle_);
LARGE_INTEGER size;
size.QuadPart = size_;
map_file_handle_ = CreateFileMapping(
INVALID_HANDLE_VALUE, // use paging file
nullptr, // default security
PAGE_READWRITE, // read/write access
size.HighPart, // maximum object size (high-order DWORD)
size.LowPart, // maximum object size (low-order DWORD)
Util::Utf8ToWideStr(name_).c_str()); // name of mapping object
if (!map_file_handle_) {
return MakeStatus("Failed to create file mapping object: %s",
Util::GetLastWin32Error());
}
// The shared memory holds the timestamps when the ports were reserved.
shared_mem_ = MapViewOfFile(map_file_handle_, // handle to map object
FILE_MAP_ALL_ACCESS, // read/write permission
0, 0, size.QuadPart);
if (!shared_mem_) {
std::string errorMessage = Util::GetLastWin32Error();
CloseHandle(map_file_handle_);
map_file_handle_ = nullptr;
return MakeStatus("Failed to map view of file: %s", errorMessage);
}
return shared_mem_;
}
~SharedMemory() {
if (shared_mem_) {
UnmapViewOfFile(shared_mem_);
shared_mem_ = nullptr;
}
if (map_file_handle_) {
CloseHandle(map_file_handle_);
map_file_handle_ = nullptr;
}
}
private:
std::string name_;
size_t size_;
HANDLE map_file_handle_ = nullptr;
void* shared_mem_ = nullptr;
};
PortManager::PortManager(std::string name, int first_port, int last_port,
ProcessFactory* process_factory,
RemoteUtil* remote_util, SystemClock* system_clock,
SteadyClock* steady_clock)
: first_port_(first_port),
last_port_(last_port),
process_factory_(process_factory),
remote_util_(remote_util),
system_clock_(system_clock),
steady_clock_(steady_clock),
shared_mem_(std::make_unique<SharedMemory>(
std::move(name), (last_port - first_port + 1) * sizeof(time_t))) {
assert(last_port_ >= first_port_);
}
PortManager::~PortManager() {
std::vector<int> ports_copy;
ports_copy.insert(ports_copy.end(), reserved_ports_.begin(),
reserved_ports_.end());
for (int port : ports_copy) {
absl::Status status = ReleasePort(port);
if (!status.ok()) {
LOG_WARNING("Failed to release port %d: %s", port, status.ToString());
}
}
}
absl::StatusOr<int> PortManager::ReservePort(int timeout_sec) {
// Find available port on workstation.
std::unordered_set<int> local_ports;
ASSIGN_OR_RETURN(local_ports,
FindAvailableLocalPorts(first_port_, last_port_, "127.0.0.1",
process_factory_, false),
"Failed to find available ports on workstation");
// Find available port on remote gamelet.
std::unordered_set<int> remote_ports;
ASSIGN_OR_RETURN(remote_ports,
FindAvailableRemotePorts(first_port_, last_port_, "0.0.0.0",
process_factory_, remote_util_,
timeout_sec, false, steady_clock_),
"Failed to find available ports on instance");
// Fetch shared memory.
void* mem;
ASSIGN_OR_RETURN(mem, shared_mem_->Get(), "Failed to get shared memory");
time_t* port_timestamps = static_cast<time_t*>(mem);
// Put ports into a multimap to iterate in LRU order.
int num_ports = last_port_ - first_port_ + 1;
std::multimap<time_t, int> ports_to_index;
for (int n = 0; n < num_ports; ++n) {
ports_to_index.insert({port_timestamps[n], n});
}
// Iterate over the ports, unused first (timestamp 0), the rest in LRU order.
// The ones with timestamps != 0 might either be stuck (e.g. process crashed
// and did not release port) or still in use.
const time_t now = std::chrono::system_clock::to_time_t(system_clock_->Now());
for (const auto& [port_timestamp, n] : ports_to_index) {
// Note that some other process might have hijacked the port in the
// meantime, hence do an InterlockedCompareExchange.
volatile time_t* ts_ptr = &port_timestamps[n];
static_assert(sizeof(time_t) == sizeof(uint64_t), "time_t must be 64 bit");
assert((reinterpret_cast<uintptr_t>(ts_ptr) & 7) == 0);
if (InterlockedCompareExchange64(ts_ptr, now, port_timestamp) ==
port_timestamp) {
int port = first_port_ + n;
LOG_DEBUG("Trying to reserve port %i", port);
// We have reserved this port. Double-check that it's actually not in use
// on both the workstation and the server.
if (local_ports.find(port) == local_ports.end()) {
LOG_DEBUG("Port %i not available on workstation", port);
InterlockedCompareExchange64(ts_ptr, now, port_timestamp);
continue;
}
if (remote_ports.find(port) == remote_ports.end()) {
LOG_DEBUG("Port %i not available on instance", port);
InterlockedCompareExchange64(ts_ptr, now, port_timestamp);
continue;
}
LOG_DEBUG("Port %i is available on workstation and instance", port);
reserved_ports_.insert(port);
return port;
}
}
return absl::ResourceExhaustedError(absl::StrFormat(
"No port available in range [%i, %i]", first_port_, last_port_));
}
absl::Status PortManager::ReleasePort(int port) {
if (reserved_ports_.find(port) == reserved_ports_.end())
return absl::OkStatus();
void* mem;
ASSIGN_OR_RETURN(mem, shared_mem_->Get(), "Failed to get shared memory");
time_t* port_timestamps = static_cast<time_t*>(mem);
volatile time_t* ts_ptr = &port_timestamps[port - first_port_];
InterlockedExchange64(ts_ptr, 0);
reserved_ports_.erase(port);
return absl::OkStatus();
}
// static
absl::StatusOr<std::unordered_set<int>> PortManager::FindAvailableLocalPorts(
int first_port, int last_port, const char* ip,
ProcessFactory* process_factory, bool forward_output_to_log) {
// -a to get the connection and ports the computer is listening on.
// -n to get numerical addresses to avoid the overhead of determining names.
// -p tcp to limit the output to TCPv4 connections.
// TODO: Use Windows API instead of netstat.
ProcessStartInfo start_info;
start_info.command = "netstat -a -n -p tcp";
start_info.name = "netstat";
std::string output;
start_info.stdout_handler = [&output](const char* data, size_t data_size) {
output.append(data, data_size);
return absl::OkStatus();
};
start_info.forward_output_to_log = forward_output_to_log;
absl::Status status = process_factory->Run(start_info);
if (!status.ok()) return WrapStatus(status, "Failed to run netstat");
LOG_DEBUG("netstat (workstation) output:\n%s", output);
return FindAvailablePorts(first_port, last_port, output, ip);
}
// static
absl::StatusOr<std::unordered_set<int>> PortManager::FindAvailableRemotePorts(
int first_port, int last_port, const char* ip,
ProcessFactory* process_factory, RemoteUtil* remote_util, int timeout_sec,
bool forward_output_to_log, SteadyClock* steady_clock) {
// --numeric to get numerical addresses.
// --listening to get only listening sockets.
// --tcp to get only TCP connections.
std::string remote_command = "netstat --numeric --listening --tcp";
ProcessStartInfo start_info =
remote_util->BuildProcessStartInfoForSsh(remote_command);
start_info.name = "netstat";
std::string output;
start_info.stdout_handler = [&output](const char* data, size_t data_size) {
output.append(data, data_size);
return absl::OkStatus();
};
start_info.forward_output_to_log = forward_output_to_log;
std::unique_ptr<Process> process = process_factory->Create(start_info);
absl::Status status = process->Start();
if (!status.ok())
return WrapStatus(status, "Failed to start netstat process");
Stopwatch timeout_timer(steady_clock);
bool is_timeout = false;
auto detect_timeout = [&timeout_timer, timeout_sec, &is_timeout]() {
is_timeout = timeout_timer.ElapsedSeconds() > timeout_sec;
return is_timeout;
};
status = process->RunUntil(detect_timeout);
if (!status.ok()) return WrapStatus(status, "Failed to run netstat process");
if (is_timeout)
return absl::DeadlineExceededError("Timeout while running netstat");
uint32_t exit_code = process->ExitCode();
if (exit_code != 0)
return MakeStatus("netstat process exited with code %u", exit_code);
LOG_DEBUG("netstat (instance) output:\n%s", output);
return FindAvailablePorts(first_port, last_port, output, ip);
}
// static
absl::StatusOr<std::unordered_set<int>> PortManager::FindAvailablePorts(
int first_port, int last_port, const std::string& netstat_output,
const char* ip) {
std::unordered_set<int> available_ports;
for (int port = first_port; port <= last_port; ++port) {
std::vector<std::string> lines = absl::StrSplit(netstat_output, '\n');
bool port_occupied = false;
std::string portToken = absl::StrFormat("%s:%i", ip, port);
for (const std::string& line : lines) {
// Ports in the TIME_WAIT state can be reused. It is common that ports
// stay in this state for O(minutes).
if (absl::StrContains(line, portToken) &&
!absl::StrContains(line, "TIME_WAIT")) {
port_occupied = true;
break;
}
}
if (!port_occupied) available_ports.insert(port);
}
if (available_ports.empty()) {
return absl::ResourceExhaustedError(absl::StrFormat(
"No port available in range [%i, %i]", first_port, last_port));
}
return available_ports;
}
} // namespace cdc_ft

145
common/process.h Normal file
View File

@@ -0,0 +1,145 @@
/*
* 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 COMMON_PROCESS_H_
#define COMMON_PROCESS_H_
#include <functional>
#include <memory>
#include <string>
#include "absl/status/status.h"
#include "common/log.h"
namespace cdc_ft {
// Helper function to forward process output to the logs.
// |name| is prefixed to the logs.
// |log_level| specifies the log level. If not given, the level is guessed from
// the log lines.
absl::Status LogOutput(const char* name, const char* data, size_t data_size,
absl::optional<LogLevel> log_level = {});
struct ProcessStartInfo {
// Handler for stdout/stderr. |data| is guaranteed to be NULL terminated, so
// it may be used like a C-string if it's known to be text, e.g. for printf().
// The NULL terminator does not count towards |data_size|.
using OutputHandler =
std::function<absl::Status(const char* data, size_t data_size)>;
// Human readable name for debugging purposes, e.g. the message pump thread
// name. Falls back to command.
std::string name;
// Command line, UTF-8 encoded.
std::string command;
// If set, the process stdin is redirected to a pipe.
// It not set, the input is connected to the stdin of the calling process.
bool redirect_stdin = false;
// If set, forwards stderr and stdout to logging unless a log handler is set.
bool forward_output_to_log = false;
// If set, these handlers get called automatically whenever stdout/stderr data
// is available. The calls happen on a WORKER THREAD, so the methods have to
// make sure the code is thread-safe. The |data| sent to the output handler
// is NULL terminated.
// If not set, the output is forwarded to the stdout/stderr of the calling
// process.
OutputHandler stdout_handler;
OutputHandler stderr_handler;
// Returns |name| if set, otherwise |command|.
const std::string& Name() const;
};
// Runs a background process and pipes stdin/stdout/stderr.
class Process {
public:
static constexpr uint32_t kExitCodeNotStarted = 4000000000;
static constexpr uint32_t kExitCodeStillRunning = 4000000001;
static constexpr uint32_t kExitCodeFailedToGetExitCode = 4000000002;
explicit Process(const ProcessStartInfo& start_info);
virtual ~Process();
// Start the background process.
virtual absl::Status Start() = 0;
// Runs the process until it exits, an error occurs (see GetStatus()) or
// |exit_condition| returns true.
virtual absl::Status RunUntil(std::function<bool()> exit_condition) = 0;
// Runs the process until it exits or an error occurs (see GetStatus()).
absl::Status RunUntilExit();
// Ends the process.
virtual absl::Status Terminate() = 0;
// Writes |data| of size |size| to the child process stdin. Only applicable
// if |redirect_stdin| was set to true in the start info, no-op if not.
virtual absl::Status WriteToStdIn(const void* data, size_t size) = 0;
// Closes the stdin pipe if |redirect_stdin| was set to true.
virtual void CloseStdIn() = 0;
// Returns true if the process has exited.
virtual bool HasExited() const = 0;
// Returns the process exit code if the process exited and the code could be
// retrieved successfully. Returns |kExitCodeNotStarted| if the process has
// not been started. Returns |kExitCodeStillRunning| if the process has not
// exited yet. Returns |kExitCodeFailedToGetExitCode| if the process exit code
// could not be retrieved.
virtual uint32_t ExitCode() const = 0;
// Returns the internal status of the process. This is an internal status. If
// the process fails with an error, this is not reported in this status, but
// instead in ExitCode().
virtual absl::Status GetStatus() const = 0;
protected:
ProcessStartInfo start_info_;
};
// Abstract process factory.
class ProcessFactory {
public:
virtual ~ProcessFactory();
// Creates a new process with given |start_info|.
virtual std::unique_ptr<Process> Create(
const ProcessStartInfo& start_info) = 0;
// Convenience method that starts a process with the given |start_info| and
// runs it until exit. Returns an error if starting or running the process
// fails, or if the exit code is not 0.
absl::Status Run(const ProcessStartInfo& start_info);
};
// Creates Windows processes.
class WinProcessFactory : public ProcessFactory {
public:
~WinProcessFactory() override;
// ProcessFactory:
std::unique_ptr<Process> Create(const ProcessStartInfo& start_info) override;
};
} // namespace cdc_ft
#endif // COMMON_PROCESS_H_

342
common/process_test.cc Normal file
View File

@@ -0,0 +1,342 @@
// 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 "common/process.h"
// Windows must be included first.
// clang-format off
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <tlhelp32.h>
// clang-format on
#include <atomic>
#include "common/log.h"
#include "common/scoped_handle_win.h"
#include "common/status_test_macros.h"
#include "common/stopwatch.h"
#include "common/util.h"
#include "gtest/gtest.h"
namespace cdc_ft {
namespace {
// Terminates a process by name.
// Returns true if the process was terminated.
// Returns false if the process was not found or if it failed to be terminated.
bool TerminateProcessByName(const char* name) {
PROCESSENTRY32 entry;
entry.dwSize = sizeof(PROCESSENTRY32);
ScopedHandle snapshot(CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL));
std::wstring wide_name = Util::Utf8ToWideStr(name);
if (Process32First(snapshot.Get(), &entry)) {
do {
if (_wcsicmp(entry.szExeFile, wide_name.c_str()) != 0) continue;
ScopedHandle hProcess(
OpenProcess(PROCESS_ALL_ACCESS, FALSE, entry.th32ProcessID));
if (!TerminateProcess(hProcess.Get(), 0)) {
LOG_ERROR("Failed to terminate process '%s'", name)
return false;
}
return true;
} while (Process32Next(snapshot.Get(), &entry));
}
LOG_ERROR("Failed to find process '%s'", name)
return false;
}
// Filters for "echos" and puts all messages and levels into a vector.
class EchosTestLog : public Log {
public:
explicit EchosTestLog(LogLevel log_level) : Log(log_level) {}
void WriteLogMessage(LogLevel level, const char* file, int line,
const char* func, const char* message) override {
if (strncmp(message, "echos", strlen("echos")) == 0) {
levels.push_back(level);
messages.push_back(message);
}
}
std::vector<LogLevel> levels;
std::vector<std::string> messages;
};
class ProcessTest : public ::testing::Test {
public:
void SetUp() override {
Log::Initialize(std::make_unique<ConsoleLog>(LogLevel::kInfo));
}
void TearDown() override { Log::Shutdown(); }
protected:
WinProcessFactory process_factory_;
};
TEST_F(ProcessTest, ProcessNotStarted) {
ProcessStartInfo start_info;
std::unique_ptr<Process> process = process_factory_.Create(start_info);
EXPECT_NOT_OK(process->RunUntilExit());
EXPECT_EQ(process->ExitCode(), Process::kExitCodeNotStarted);
}
TEST_F(ProcessTest, RunSimpleCommandSucceeds) {
ProcessStartInfo start_info;
start_info.command = "cmd /C \"echo\"";
std::unique_ptr<Process> process = process_factory_.Create(start_info);
EXPECT_OK(process->Start());
EXPECT_OK(process->RunUntilExit());
EXPECT_TRUE(process->HasExited());
EXPECT_EQ(process->ExitCode(), 0u);
}
TEST_F(ProcessTest, RunSimpleCommandFails) {
ProcessStartInfo start_info;
start_info.command = "cmd /C \"dir /INVALID\"";
std::unique_ptr<Process> process = process_factory_.Create(start_info);
EXPECT_OK(process->Start());
EXPECT_OK(process->RunUntilExit());
EXPECT_TRUE(process->HasExited());
EXPECT_EQ(process->ExitCode(), 1u);
}
TEST_F(ProcessTest, RunAndReadStdOut) {
std::string std_out;
std::string message = "hello world";
ProcessStartInfo start_info;
start_info.command = "cmd /C \"echo " + message + "\"";
start_info.stdout_handler = [&std_out](const char* data, size_t) {
std_out += data;
return absl::OkStatus();
};
std::unique_ptr<Process> process = process_factory_.Create(start_info);
EXPECT_OK(process->Start());
EXPECT_OK(process->RunUntilExit());
EXPECT_TRUE(process->HasExited());
EXPECT_EQ(process->ExitCode(), 0u);
EXPECT_EQ(std_out, message + "\r\n");
}
TEST_F(ProcessTest, RunAndReadStdOutAndStdErr) {
std::string std_out;
std::string std_err;
std::string stdout_message = "stdout message";
std::string stderr_message = "stderr message";
ProcessStartInfo start_info;
start_info.command = "cmd /C \"echo " + stdout_message + " & echo " +
stderr_message + " 1>&2\"";
start_info.stdout_handler = [&std_out](const char* data, size_t) {
std_out += data;
return absl::OkStatus();
};
start_info.stderr_handler = [&std_err](const char* data, size_t) {
std_err += data;
return absl::OkStatus();
};
std::unique_ptr<Process> process = process_factory_.Create(start_info);
EXPECT_OK(process->Start());
EXPECT_OK(process->RunUntilExit());
EXPECT_TRUE(process->HasExited());
EXPECT_EQ(process->ExitCode(), 0u);
EXPECT_EQ(std_out, stdout_message + " \r\n");
EXPECT_EQ(std_err, stderr_message + " \r\n");
}
TEST_F(ProcessTest, RunWriteStdInAndReadStdOut) {
std::string std_out;
std::string message = "Foo\nBar\nfoo\nbar\nFar\nBoo";
// Find all lines containing "F".
ProcessStartInfo start_info;
start_info.command = "findstr F";
start_info.redirect_stdin = true;
start_info.stdout_handler = [&std_out](const char* data, size_t) {
std_out += data;
return absl::OkStatus();
};
std::unique_ptr<Process> process = process_factory_.Create(start_info);
EXPECT_OK(process->Start());
EXPECT_OK(process->WriteToStdIn(message.data(), message.size()));
process->CloseStdIn();
EXPECT_OK(process->RunUntilExit());
EXPECT_TRUE(process->HasExited());
EXPECT_EQ(process->ExitCode(), 0u);
EXPECT_EQ(std_out, "Foo\nFar\n");
}
TEST_F(ProcessTest, RunIoStressTest) {
std::string std_out;
std::string expected_stdout;
// Find all lines containing "1".
ProcessStartInfo start_info;
start_info.command = "findstr 1";
start_info.redirect_stdin = true;
start_info.stdout_handler = [&std_out](const char* data, size_t) {
std_out += data;
return absl::OkStatus();
};
std::unique_ptr<Process> process = process_factory_.Create(start_info);
EXPECT_OK(process->Start());
// Write lots of lines to stdin.
// Every other line starts with '1' and should be picked up by findstr.
std::string message(2048, 'x');
message.back() = '\n';
const int num_lines = 1000;
for (int n = 0; n < num_lines; ++n) {
message[0] = n & 1 ? '1' : '2';
if (n & 1) {
expected_stdout += message;
}
EXPECT_OK(process->WriteToStdIn(message.data(), message.size()));
}
process->CloseStdIn();
EXPECT_OK(process->RunUntilExit());
EXPECT_TRUE(process->HasExited());
EXPECT_EQ(process->ExitCode(), 0u);
EXPECT_EQ(std_out, expected_stdout);
}
TEST_F(ProcessTest, RunUntil) {
std::string std_out;
// Find all lines containing "msg". "findstr" is a convenient command that
// echoes stdin to stdout for matching lines. "echo" doesn't work with stdin.
ProcessStartInfo start_info;
start_info.command = "findstr stop";
start_info.redirect_stdin = true;
std::atomic_bool stop(false);
start_info.stdout_handler = [&std_out, &stop](const char* data, size_t) {
// Check whether someone sent the "stop" command.
// Note: This runs in a background thread.
std_out += data;
stop = std_out.find("stop") != std::string::npos;
return absl::OkStatus();
};
std::unique_ptr<Process> process = process_factory_.Create(start_info);
EXPECT_OK(process->Start());
// Send "msg stop" and check that it stops.
std::string message = " stop\n";
EXPECT_OK(process->WriteToStdIn(message.data(), message.size()));
process->CloseStdIn();
EXPECT_OK(process->RunUntil([&stop]() { return stop.load(); }));
EXPECT_TRUE(stop);
EXPECT_OK(process->RunUntilExit());
EXPECT_TRUE(process->HasExited());
EXPECT_EQ(process->ExitCode(), 0u);
}
TEST_F(ProcessTest, ForwardOutputToLogging) {
Log::Shutdown();
auto log_ptr = std::make_unique<EchosTestLog>(LogLevel::kInfo);
EchosTestLog* log = log_ptr.get();
Log::Initialize(std::move(log_ptr));
const std::string stdout_message = "stdout message";
const std::string stderr_message = "stderr message";
ProcessStartInfo start_info;
start_info.command = "cmd /C \"echo " + stdout_message + " & echo " +
stderr_message + " 1>&2\"";
start_info.forward_output_to_log = true;
start_info.name = "echos";
EXPECT_OK(process_factory_.Run(start_info));
ASSERT_EQ(log->messages.size(), 2);
EXPECT_EQ(log->messages[0], "echos_stdout: " + stdout_message + " ");
EXPECT_EQ(log->messages[1], "echos_stderr: " + stderr_message + " ");
}
TEST_F(ProcessTest, LogOutputLevelDetection) {
Log::Shutdown();
auto log_ptr = std::make_unique<EchosTestLog>(LogLevel::kVerbose);
EchosTestLog* log = log_ptr.get();
Log::Initialize(std::move(log_ptr));
ProcessStartInfo start_info;
start_info.command = "cmd /C \"echo VERBOSE msg1 && ";
start_info.command += "echo DEBUG msg2 && ";
start_info.command += "echo INFO msg3 && ";
start_info.command += "echo WARNING msg4 && ";
start_info.command += "echo ERROR msg5 && ";
start_info.command += "echo msg6\"";
start_info.forward_output_to_log = true;
start_info.name = "echos";
EXPECT_OK(process_factory_.Run(start_info));
ASSERT_EQ(log->messages.size(), 6);
ASSERT_EQ(log->levels.size(), 6);
EXPECT_EQ(log->messages[0], "echos_stdout: VERBOSE msg1 ");
EXPECT_EQ(log->messages[1], "echos_stdout: DEBUG msg2 ");
EXPECT_EQ(log->messages[2], "echos_stdout: INFO msg3 ");
EXPECT_EQ(log->messages[3], "echos_stdout: WARNING msg4 ");
EXPECT_EQ(log->messages[4], "echos_stdout: ERROR msg5 ");
EXPECT_EQ(log->messages[5], "echos_stdout: msg6");
EXPECT_EQ(log->levels[0], LogLevel::kVerbose);
EXPECT_EQ(log->levels[1], LogLevel::kDebug);
EXPECT_EQ(log->levels[2], LogLevel::kInfo);
EXPECT_EQ(log->levels[3], LogLevel::kWarning);
EXPECT_EQ(log->levels[4], LogLevel::kError);
EXPECT_EQ(log->levels[5], LogLevel::kInfo);
}
TEST_F(ProcessTest, Terminate) {
ProcessStartInfo start_info;
start_info.command = "timeout /T 30";
std::unique_ptr<Process> process = process_factory_.Create(start_info);
EXPECT_OK(process->Start());
EXPECT_EQ(process->ExitCode(), Process::kExitCodeStillRunning);
EXPECT_OK(process->Terminate());
EXPECT_EQ(process->ExitCode(), Process::kExitCodeNotStarted);
}
TEST_F(ProcessTest, TerminateAlreadyExited) {
ProcessStartInfo start_info;
start_info.command = "timeout /T 30";
std::unique_ptr<Process> process = process_factory_.Create(start_info);
EXPECT_OK(process->Start());
EXPECT_FALSE(process->HasExited());
bool terminated = false;
Stopwatch sw;
while (sw.ElapsedSeconds() < 5 && !terminated) {
terminated = TerminateProcessByName("timeout.exe");
if (!terminated) Util::Sleep(1);
}
EXPECT_TRUE(terminated);
EXPECT_OK(process->Terminate());
}
} // namespace
} // namespace cdc_ft

848
common/process_win.cc Normal file
View File

@@ -0,0 +1,848 @@
// 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 "common/process.h"
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <atomic>
#include <cassert>
#include <mutex>
#include <thread>
#include <vector>
#include "absl/strings/str_format.h"
#include "common/scoped_handle_win.h"
#include "common/status.h"
#include "common/util.h"
namespace cdc_ft {
namespace {
// SetThreadDescription is not available on all Windows versions,
// e.g. Win Server 2016, which happens to run on YRunners!.
typedef HRESULT(WINAPI* SetThreadDescription)(HANDLE hThread,
PCWSTR lpThreadDescription);
// Sets the name of the current thread to |name|.
// Works from Windows 10 version 1607 on, no-op otherwise.
void SetThreadName(const std::string& name) {
static auto set_thread_description_func =
reinterpret_cast<SetThreadDescription>(::GetProcAddress(
::GetModuleHandle(L"Kernel32.dll"), "SetThreadDescription"));
if (set_thread_description_func) {
set_thread_description_func(::GetCurrentThread(),
Util::Utf8ToWideStr(name).c_str());
}
}
std::atomic_int g_pipe_serial_number(0);
// Creates a pipe suitable for overlapped IO. Regular anonymous pipes in Windows
// don't support overlapped IO. This method creates a named pipe with a unique
// name, sets it up for overlapped IO and returns read/write ends.
absl::Status CreatePipeForOverlappedIo(ScopedHandle* pipe_read_end,
ScopedHandle* pipe_write_end) {
// We need named pipes for overlapped IO, so create a unique name.
int id = g_pipe_serial_number++;
std::string pipe_name = absl::StrFormat(
R"(\\.\Pipe\GgpRsyncIoPipe.%08x.%08x)", GetCurrentProcessId(), id);
// Set the bInheritHandle flag so pipe handles are inherited.
SECURITY_ATTRIBUTES security_attributes;
security_attributes.nLength = sizeof(SECURITY_ATTRIBUTES);
security_attributes.bInheritHandle = TRUE;
security_attributes.lpSecurityDescriptor = nullptr;
*pipe_read_end = ScopedHandle(CreateNamedPipeA(
pipe_name.c_str(), PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED,
PIPE_TYPE_BYTE | PIPE_WAIT,
1, // Number of pipes
4096, // Out buffer size
4096, // In buffer size
120 * 1000, // Timeout in ms
&security_attributes));
if (!pipe_read_end->IsValid()) {
return MakeStatus("Failed to create pipe read end: %s",
Util::GetLastWin32Error());
}
*pipe_write_end =
ScopedHandle(CreateFileA(pipe_name.c_str(), GENERIC_WRITE,
0, // No sharing
&security_attributes, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
nullptr)); // Template file
if (!pipe_write_end->IsValid()) {
// Note that Close() might change GetLastErrorString()!
absl::Status status = MakeStatus("Failed to create pipe write end: %s",
Util::GetLastWin32Error());
pipe_read_end->Close();
return status;
}
return absl::OkStatus();
}
// Creates a pipe intended for piping stdin from this process to a child
// process. The read end is inherited to the child process (so it can read
// stdin from it), the write end is not. The pipe is NOT suitable for async IO.
absl::Status SetUpInputPipe(ScopedHandle* read_end, ScopedHandle* write_end) {
// Set the bInheritHandle flag so pipe handles are inherited.
SECURITY_ATTRIBUTES security_attributes;
security_attributes.nLength = sizeof(SECURITY_ATTRIBUTES);
security_attributes.bInheritHandle = TRUE;
security_attributes.lpSecurityDescriptor = nullptr;
// Create pipe.
HANDLE read_end_handle, write_end_handle;
if (!CreatePipe(&read_end_handle, &write_end_handle, &security_attributes,
4096)) {
return MakeStatus("Failed to create input pipes");
}
*read_end = ScopedHandle(read_end_handle);
*write_end = ScopedHandle(write_end_handle);
// Ensure the write end of the pipe is not inherited to the child process.
if (!SetHandleInformation(write_end->Get(), HANDLE_FLAG_INHERIT, 0)) {
// Note that Close() might change GetLastErrorString()!
absl::Status status = MakeStatus("Failed to set handle information: %s",
Util::GetLastWin32Error());
read_end->Close();
write_end->Close();
return status;
}
return absl::OkStatus();
}
// Creates a pipe intended for piping stdout/stderr from a child process to this
// process. The write end is inherited to the child process (so it can write
// stdout/stderr to it), the read end is not. The pipe is suitable for async IO.
absl::Status SetUpOutputPipe(ScopedHandle* read_end, ScopedHandle* write_end) {
// Create pipe.
absl::Status status = CreatePipeForOverlappedIo(read_end, write_end);
if (!status.ok()) {
return WrapStatus(status, "Failed to create output pipes");
}
// Ensure the read end of the pipe is not inherited to the child process.
if (!SetHandleInformation(read_end->Get(), HANDLE_FLAG_INHERIT, 0)) {
// Note that Close() might change GetLastErrorString()!
status = MakeStatus("Failed to set handle information: %s",
Util::GetLastWin32Error());
read_end->Close();
write_end->Close();
return status;
}
return absl::OkStatus();
}
// Helper class for performing async IO from the stdout/stderr pipes created by
// SetUpOutputPipe().
class AsyncReader {
public:
using OutputHandler = ProcessStartInfo::OutputHandler;
AsyncReader(HANDLE pipe_handle, OutputHandler output_handler)
: pipe_handle_(pipe_handle),
output_handler_(std::move(output_handler)),
buffer_(4096) {
ZeroMemory(&overlapped_, sizeof(overlapped_));
}
~AsyncReader() {
// Better cancel pending IO before the buffers get deleted. I heard from a
// "friend" that they got a heap corruption when they didn't do it.
absl::Status status = CancelPendingIo();
if (!status.ok()) {
LOG_WARNING("%s", status.ToString());
}
}
// Returns the event that is triggered when async IO completes.
HANDLE GetEvent() const { return event_.Get(); }
// Initialize the IO event (see GetEvent()) and issue an async IO request.
absl::Status Initialize() {
// Create signaled manual reset event.
event_ = ScopedHandle(CreateEvent(nullptr, TRUE, TRUE, nullptr));
if (!event_.IsValid()) {
return MakeStatus("CreateEvent failed: %s", Util::GetLastWin32Error());
}
overlapped_.hEvent = event_.Get();
// Start reading.
absl::Status status = IssueRead();
if (!status.ok()) {
return WrapStatus(status, "IssueRead() failed");
}
return absl::OkStatus();
}
// Reads the result of the async IO request. Async IO must be pending.
// Should be called if the IO event (see GetEvent()) was triggered, otherwise
// the method will block until the async IO result is available.
absl::Status Read() {
assert(io_pending_);
io_pending_ = false;
DWORD num_bytes_read;
if (!GetOverlappedResult(pipe_handle_, &overlapped_, &num_bytes_read,
true)) {
switch (GetLastError()) {
case ERROR_BROKEN_PIPE:
// The pipe was closed by the child process. Set the EOF() marker.
LOG_VERBOSE("EOF");
eof_ = true;
break;
default:
return MakeStatus("GetOverlappedResult() failed: %s",
Util::GetLastWin32Error());
}
} else {
// Async IO succeeded. Append null terminator in case the handler accesses
// the data like a C string.
assert(num_bytes_read < buffer_.size());
buffer_[num_bytes_read] = 0;
absl::Status status = output_handler_(buffer_.data(), num_bytes_read);
if (!status.ok()) {
// Don't return an error, it stops the pump thread and leads to freezes.
LOG_DEBUG("%s", WrapStatus(status, "Output handler failed").ToString());
}
}
if (!ResetEvent(event_.Get())) {
return MakeStatus("ResetEvent() failed: %s", Util::GetLastWin32Error());
}
// Only issue a new read if the pipe is still open.
if (!eof_) {
absl::Status status = IssueRead();
if (!status.ok()) {
return WrapStatus(status, "IssueRead() failed");
}
}
return absl::OkStatus();
}
// Cancels the currently pending async IO request if there is any.
// Must be called from the same thread as Initialize() and Read().
absl::Status CancelPendingIo() {
if (!io_pending_) {
return absl::OkStatus();
}
if (!CancelIo(pipe_handle_)) {
return MakeStatus(
"CancelIo() failed. If you get a heap corruption, this is why.");
}
io_pending_ = false;
return absl::OkStatus();
}
private:
// Queues a new async IO request. If data is already available, immediately
// calls the output handler. Returns false on error (failing output handler,
// IO error).
absl::Status IssueRead() {
assert(!io_pending_);
// The pipe might already contain data that can be read synchronously. Just
// keep reading it.
DWORD num_bytes_read;
while (ReadFile(pipe_handle_, buffer_.data(),
static_cast<DWORD>(buffer_.size()) - 1, &num_bytes_read,
&overlapped_)) {
// Append a null terminator in case handler interprets the data as C str.
assert(num_bytes_read < buffer_.size());
buffer_[num_bytes_read] = 0;
absl::Status status = output_handler_(buffer_.data(), num_bytes_read);
if (!status.ok()) {
// Don't return an error, it stops the pump thread and leads to freezes.
LOG_DEBUG("%s", WrapStatus(status, "Output handler failed").ToString());
}
}
switch (GetLastError()) {
case ERROR_IO_PENDING:
// Async IO in progress, this is expected. The caller should wait for
// the event (see GetEvent()) to retrieve the data.
io_pending_ = true;
return absl::OkStatus();
case ERROR_BROKEN_PIPE:
// The pipe was closed by the child process. Set the EOF() marker.
LOG_VERBOSE("EOF");
eof_ = true;
return absl::OkStatus();
default:
return MakeStatus("ReadFile failed: %s", Util::GetLastWin32Error());
}
}
// Not owned.
HANDLE pipe_handle_;
OutputHandler output_handler_;
std::vector<char> buffer_;
OVERLAPPED overlapped_;
ScopedHandle event_;
bool eof_ = false;
bool io_pending_ = false;
};
struct ProcessInfo {
PROCESS_INFORMATION pi;
ScopedHandle job;
ScopedHandle stdin_write_end;
ScopedHandle stdout_read_end;
ScopedHandle stderr_read_end;
ProcessInfo() { ZeroMemory(&pi, sizeof(pi)); }
};
// Background thread to read stdout/stderr from the child process.
// Also watches the child process for exit.
class MessagePumpThread {
public:
MessagePumpThread(const ProcessInfo& process_info,
const ProcessStartInfo& start_info)
: process_handle_(process_info.pi.hProcess), name_(start_info.Name()) {
// Initialize stdout reader if necessary.
if (process_info.stdout_read_end.IsValid()) {
stdout_reader_ = std::make_unique<AsyncReader>(
process_info.stdout_read_end.Get(), start_info.stdout_handler);
}
// Initialize stderr reader if necessary.
if (process_info.stderr_read_end.IsValid()) {
stderr_reader_ = std::make_unique<AsyncReader>(
process_info.stderr_read_end.Get(), start_info.stderr_handler);
}
// Create manual reset event that is not signaled.
shutdown_event_ = ScopedHandle(CreateEvent(nullptr, TRUE, FALSE, nullptr));
worker_ = std::thread([this]() { ThreadWorkerMain(); });
}
~MessagePumpThread() { Shutdown(); }
void Shutdown() {
if (shutdown_event_.IsValid() && !SetEvent(shutdown_event_.Get())) {
// Can't do much if this fails.
LOG_ERROR("Shutting down process message thread failed");
exit(1);
}
if (worker_.joinable()) {
worker_.join();
}
}
// Contains the error message if some error occurred and the message pump
// thread was shut down.
absl::Status GetStatus() {
std::lock_guard<std::mutex> lock(status_mutex_);
return status_;
}
// Returns true if the process has exited.
bool HasExited() {
std::lock_guard<std::mutex> lock(exit_mutex_);
return has_exited_;
}
// Returns the process exit code.
uint32_t ExitCode() {
std::lock_guard<std::mutex> lock(exit_mutex_);
return exit_code_;
}
private:
void SetStatus(absl::Status status) {
LOG_DEBUG("Setting status %s of process %s", status.ToString().c_str(),
name_.c_str());
std::lock_guard<std::mutex> lock(status_mutex_);
status_ = status;
}
void ThreadWorkerMain() {
SetThreadName(name_);
LOG_VERBOSE("Process message thread started: %s", name_.c_str());
if (!shutdown_event_.IsValid()) {
SetStatus(
MakeStatus("CreateEvent failed: %s", Util::GetLastWin32Error()));
return;
}
// Be sure to call AsyncReader::Initialize() from this thread, so all
// AsyncIO stays here.
// Initialize stdout reader if present (schedules AsyncIO).
if (stdout_reader_) {
absl::Status init_status = stdout_reader_->Initialize();
if (!init_status.ok()) {
SetStatus(
WrapStatus(init_status, "Failed to initialize stdout reader"));
return;
}
}
// Initialize stderr reader if present (schedules AsyncIO).
if (stderr_reader_) {
absl::Status init_status = stderr_reader_->Initialize();
if (!init_status.ok()) {
SetStatus(
WrapStatus(init_status, "Failed to initialize stderr reader"));
return;
}
}
// Initialize handles to watch.
std::vector<HANDLE> watch_handles;
size_t process_index = watch_handles.size();
watch_handles.push_back(process_handle_);
size_t shutdown_index = watch_handles.size();
watch_handles.push_back(shutdown_event_.Get());
size_t stdout_index = SIZE_MAX;
if (stdout_reader_) {
stdout_index = watch_handles.size();
watch_handles.push_back(stdout_reader_->GetEvent());
}
size_t stderr_index = SIZE_MAX;
if (stderr_reader_) {
stderr_index = watch_handles.size();
watch_handles.push_back(stderr_reader_->GetEvent());
}
for (;;) {
const uint32_t wait_index =
WaitForMultipleObjects(static_cast<DWORD>(watch_handles.size()),
watch_handles.data(), false, UINT_MAX);
if (wait_index == WAIT_TIMEOUT) {
continue;
}
const uint32_t handle_index = wait_index - WAIT_OBJECT_0;
if (handle_index >= watch_handles.size()) {
SetStatus(MakeStatus(
"WaitForMultipleObjects failed with invalid handle index %u",
handle_index));
return;
}
if (handle_index == process_index) {
// Process exited.
std::lock_guard<std::mutex> lock(exit_mutex_);
has_exited_ = true;
// Get process exit code.
if (!GetExitCodeProcess(process_handle_, &exit_code_)) {
LOG_WARNING("Failed to get exit code for process '%s': %s", name_,
Util::GetLastWin32Error());
exit_code_ = Process::kExitCodeFailedToGetExitCode;
}
break;
}
if (handle_index == shutdown_index) {
// Shutdown() was called.
break;
}
if (handle_index == stdout_index) {
// Data on stdout is available.
absl::Status status = stdout_reader_->Read();
if (!status.ok()) {
SetStatus(WrapStatus(status, "Failed to read stdout"));
return;
}
continue;
}
if (handle_index == stderr_index) {
// Data on stderr is available.
absl::Status status = stderr_reader_->Read();
if (!status.ok()) {
SetStatus(WrapStatus(status, "Failed to read stderr"));
return;
}
continue;
}
}
// Cancel any pending IO.
stdout_reader_.reset();
stderr_reader_.reset();
LOG_VERBOSE("Process message thread stopped: %s", name_.c_str());
}
HANDLE process_handle_;
std::string name_;
std::thread worker_;
ScopedHandle shutdown_event_;
absl::Status status_;
std::mutex status_mutex_;
bool has_exited_ = false;
DWORD exit_code_ = Process::kExitCodeStillRunning;
std::mutex exit_mutex_;
std::unique_ptr<AsyncReader> stdout_reader_;
std::unique_ptr<AsyncReader> stderr_reader_;
};
// Try to guess the log level from |str|, e.g. LogLevel::kError if |str| starts
// with "ERROR".
LogLevel GuessLogLevel(const char* str) {
if (strncmp(str, "ERROR", 5) == 0)
return LogLevel::kError;
else if (strncmp(str, "WARNING", 7) == 0)
return LogLevel::kWarning;
else if (strncmp(str, "DEBUG", 5) == 0)
return LogLevel::kDebug;
else if (strncmp(str, "VERBOSE", 7) == 0)
return LogLevel::kVerbose;
return LogLevel::kInfo;
}
} // namespace
absl::Status LogOutput(const char* name, const char* data, size_t /*size*/,
absl::optional<LogLevel> log_level) {
const char* newline_pos = strpbrk(data, "\r\n");
while (newline_pos) {
if (newline_pos > data) {
LOG_LEVEL(log_level ? *log_level : GuessLogLevel(data), "%s: %s", name,
std::string(data, newline_pos - data));
}
data = newline_pos + 1;
newline_pos = strpbrk(data, "\r\n");
}
// There's always guaranteed to be NULL terminator, even if |size| is 0.
// Note that the rest here might be an incomplete line, but we print it,
// anyway, to not risk loosing data.
if (data[0] != 0) {
LOG_LEVEL(log_level ? *log_level : GuessLogLevel(data), "%s: %s", name,
data);
}
return absl::OkStatus();
}
const std::string& ProcessStartInfo::Name() const {
return !name.empty() ? name : command;
}
Process::Process(const ProcessStartInfo& start_info)
: start_info_(start_info) {}
Process::~Process() = default;
absl::Status Process::RunUntilExit() {
return RunUntil([]() { return false; });
}
// Implementation of a Windows process.
class WinProcess : public Process {
public:
explicit WinProcess(const ProcessStartInfo& start_info);
~WinProcess() override;
// Process:
absl::Status Start() override;
absl::Status RunUntil(std::function<bool()> exit_condition) override;
absl::Status Terminate() override;
absl::Status WriteToStdIn(const void* data, size_t size) override;
void CloseStdIn() override;
bool HasExited() const override;
uint32_t ExitCode() const override;
absl::Status GetStatus() const override;
private:
std::unique_ptr<ProcessInfo> process_info_;
std::unique_ptr<MessagePumpThread> message_pump_;
};
WinProcess::WinProcess(const ProcessStartInfo& start_info)
: Process(start_info) {}
WinProcess::~WinProcess() { Terminate().IgnoreError(); }
absl::Status WinProcess::Start() {
LOG_INFO("Starting process %s", start_info_.command.c_str());
std::wstring command = Util::Utf8ToWideStr(start_info_.command);
wchar_t* command_cstr = const_cast<wchar_t*>(command.c_str());
STARTUPINFO si;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
process_info_ = std::make_unique<ProcessInfo>();
// Create stdout pipes if necessary.
ScopedHandle stdin_read_end;
if (start_info_.redirect_stdin) {
absl::Status status =
SetUpInputPipe(&stdin_read_end, &process_info_->stdin_write_end);
if (!status.ok()) {
return WrapStatus(status, "Failed to set up stdin pipes");
}
}
// Set up handlers to forward output to logging.
if (!start_info_.stdout_handler && start_info_.forward_output_to_log) {
start_info_.stdout_handler = [name = start_info_.Name() + "_stdout"](
const char* data, size_t size) {
return LogOutput(name.c_str(), data, size);
};
}
if (!start_info_.stderr_handler && start_info_.forward_output_to_log) {
start_info_.stderr_handler = [name = start_info_.Name() + "_stderr"](
const char* data, size_t size) {
return LogOutput(name.c_str(), data, size, LogLevel::kError);
};
}
// Create stdout pipes if necessary.
ScopedHandle stdout_write_end;
if (start_info_.stdout_handler) {
absl::Status status =
SetUpOutputPipe(&process_info_->stdout_read_end, &stdout_write_end);
if (!status.ok()) {
return WrapStatus(status, "Failed to set up stdout pipes");
}
}
// Create stderr pipes if necessary.
ScopedHandle stderr_write_end;
if (start_info_.stderr_handler) {
absl::Status status =
SetUpOutputPipe(&process_info_->stderr_read_end, &stderr_write_end);
if (!status.ok()) {
return WrapStatus(status, "Failed to set up stderr pipes");
}
}
// Set up handle redirection. Note that it's not possible to redirect only
// some handles, so use GetStdHandle() for the ones not redirected.
si.dwFlags |= STARTF_USESTDHANDLES;
si.hStdInput = stdin_read_end.IsValid() ? stdin_read_end.Get()
: GetStdHandle(STD_INPUT_HANDLE);
si.hStdOutput = stdout_write_end.IsValid() ? stdout_write_end.Get()
: GetStdHandle(STD_OUTPUT_HANDLE);
si.hStdError = stderr_write_end.IsValid() ? stderr_write_end.Get()
: GetStdHandle(STD_ERROR_HANDLE);
// Make sure the process gets closed if the parent (this!) process exits.
process_info_->job = ScopedHandle(CreateJobObject(NULL, NULL));
if (!process_info_->job.IsValid()) {
return MakeStatus("CreateJobObject() failed: %s",
Util::GetLastWin32Error());
}
JOBOBJECT_EXTENDED_LIMIT_INFORMATION jeli = {0};
jeli.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
bool success = SetInformationJobObject(process_info_->job.Get(),
JobObjectExtendedLimitInformation,
&jeli, sizeof(jeli));
if (!success) {
return MakeStatus("SetInformationJobObject() failed: %s",
Util::GetLastWin32Error());
}
// Start the child process.
success = CreateProcess(NULL, // No module name (use command line)
command_cstr,
NULL, // Process handle not inheritable
NULL, // Thread handle not inheritable
TRUE, // Inherit handles
0, // No creation flags
NULL, // Use parent's environment block
NULL, // Use parent's starting directory
&si, &process_info_->pi);
if (!success) {
return MakeStatus("CreateProcess() failed: %s", Util::GetLastWin32Error());
}
success = AssignProcessToJobObject(process_info_->job.Get(),
process_info_->pi.hProcess);
if (!success) {
return MakeStatus("AssignProcessToJobObject() failed: %s",
Util::GetLastWin32Error());
}
// Explicitly close our copies of the input read end and output write ends.
// The child process still has a copy.
stdin_read_end.Close();
stdout_write_end.Close();
stderr_write_end.Close();
// Start message pump thread.
message_pump_ =
std::make_unique<MessagePumpThread>(*process_info_, start_info_);
return absl::OkStatus();
}
absl::Status WinProcess::RunUntil(std::function<bool()> exit_condition) {
if (!process_info_) {
return MakeStatus("Process was never started");
}
// Wait until exit condition is fulfilled.
while (!exit_condition()) {
// Poll |message_pump_|. This is not super performance critical, so a simple
// sleep should do.
Util::Sleep(10);
absl::Status status = message_pump_->GetStatus();
if (!status.ok()) return status;
if (HasExited()) return absl::OkStatus();
}
return absl::OkStatus();
}
absl::Status WinProcess::WriteToStdIn(const void* data, size_t size) {
if (!start_info_.redirect_stdin) {
return absl::FailedPreconditionError("Stdin not redirected");
}
DWORD bytes_written = 0;
assert(size <= UINT32_MAX);
const DWORD dw_size = static_cast<DWORD>(size);
if (!WriteFile(process_info_->stdin_write_end.Get(), data, dw_size,
&bytes_written, nullptr)) {
return MakeStatus("WriteFile() failed: %s", Util::GetLastWin32Error());
}
if (bytes_written != size) {
return MakeStatus("WriteFile() failed: Only %u / %u bytes written",
bytes_written, size);
}
return absl::OkStatus();
}
void WinProcess::CloseStdIn() {
assert(start_info_.redirect_stdin);
process_info_->stdin_write_end.Close();
}
bool WinProcess::HasExited() const {
return message_pump_ ? message_pump_->HasExited() : false;
}
uint32_t WinProcess::ExitCode() const {
return message_pump_ ? message_pump_->ExitCode() : kExitCodeNotStarted;
}
absl::Status WinProcess::GetStatus() const {
return message_pump_ ? message_pump_->GetStatus() : absl::OkStatus();
}
absl::Status WinProcess::Terminate() {
// Stop message pump.
bool should_terminate = false;
if (message_pump_) {
should_terminate = !message_pump_->HasExited();
message_pump_.reset();
}
if (process_info_) {
bool result = true;
if (should_terminate) {
result = TerminateProcess(process_info_->pi.hProcess, 0);
if (!result && GetLastError() == ERROR_ACCESS_DENIED) {
// This means that the process has already exited, but in a way that
// the exit wasn't properly reported to this code (e.g. the process got
// killed somewhere). Just handle this silently.
LOG_DEBUG("Process '%s' already exited", start_info_.Name());
result = true;
}
}
// Close the handles that are not scoped handles.
ScopedHandle(process_info_->pi.hProcess).Close();
ScopedHandle(process_info_->pi.hThread).Close();
process_info_.reset();
if (!result) {
return MakeStatus("TerminateProcess() failed: %s",
Util::GetLastWin32Error());
}
}
return absl::OkStatus();
}
ProcessFactory::~ProcessFactory() = default;
absl::Status ProcessFactory::Run(const ProcessStartInfo& start_info) {
std::unique_ptr<Process> process = Create(start_info);
absl::Status status = process->Start();
if (!status.ok()) {
return WrapStatus(status, "Failed to start process '%s'",
start_info.Name());
}
status = process->RunUntilExit();
if (!status.ok()) {
return WrapStatus(status, "Failed to run process '%s'", start_info.Name());
}
uint32_t exit_code = process->ExitCode();
if (exit_code != 0) {
return MakeStatus("Process '%s' exited with code %u", start_info.Name(),
exit_code);
}
return absl::OkStatus();
}
WinProcessFactory::~WinProcessFactory() = default;
std::unique_ptr<Process> WinProcessFactory::Create(
const ProcessStartInfo& start_info) {
return std::unique_ptr<Process>(new WinProcess(start_info));
}
} // namespace cdc_ft

229
common/remote_util.cc Normal file
View File

@@ -0,0 +1,229 @@
// 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 "common/remote_util.h"
#include <atomic>
#include <regex>
#include <sstream>
#include "absl/strings/str_format.h"
#include "common/path.h"
#include "common/status.h"
namespace cdc_ft {
namespace {
// Escapes command line argument for the Microsoft command line parser in
// preparation for quoting. Double quotes are backslash-escaped. Literal
// backslashes are backslash-escaped if they are followed by a double quote, or
// if they are part of a sequence of backslashes that are followed by a double
// quote.
std::string EscapeForWindows(const std::string& argument) {
std::string str =
std::regex_replace(argument, std::regex(R"(\\*(?=""|$))"), "$1$1");
return std::regex_replace(str, std::regex("\""), "\\\"");
}
// Quotes and escapes a command line argument following the convention
// understood by the Microsoft command line parser.
std::string QuoteArgument(const std::string& argument) {
return absl::StrFormat("\"%s\"", EscapeForWindows(argument));
}
// Quotes and escapes a command line arguments for use in ssh command. The
// argument is first escaped and quoted for Linux using single quotes and then
// it is escaped to be used by the Microsoft command line parser.
std::string QuoteAndEscapeArgumentForSsh(const std::string& argument) {
std::string quoted_argument = absl::StrFormat(
"'%s'", std::regex_replace(argument, std::regex("'"), "'\\''"));
return EscapeForWindows(quoted_argument);
}
// Gets the argument for SSH (reverse) port forwarding, e.g. -L23:localhost:45.
std::string GetPortForwardingArg(int local_port, int remote_port,
bool reverse) {
if (reverse)
return absl::StrFormat("-R%i:localhost:%i ", remote_port, local_port);
return absl::StrFormat("-L%i:localhost:%i ", local_port, remote_port);
}
} // namespace
RemoteUtil::RemoteUtil(int verbosity, bool quiet,
ProcessFactory* process_factory,
bool forward_output_to_log)
: verbosity_(verbosity),
quiet_(quiet),
process_factory_(process_factory),
forward_output_to_log_(forward_output_to_log) {}
void RemoteUtil::SetIpAndPort(const std::string& gamelet_ip, int ssh_port) {
gamelet_ip_ = gamelet_ip;
ssh_port_ = ssh_port;
}
absl::Status RemoteUtil::Scp(std::vector<std::string> source_filepaths,
const std::string& dest, bool compress) {
absl::Status status = CheckIpPort();
if (!status.ok()) {
return status;
}
std::string source_args;
for (const std::string& sourceFilePath : source_filepaths) {
source_args += QuoteArgument(sourceFilePath) + " ";
}
// -p preserves timestamps. This enables timestamp-based up-to-date checks.
ProcessStartInfo start_info;
start_info.command = absl::StrFormat(
"%s "
"%s %s -p -T "
"-F %s "
"-i %s -P %i "
"-oStrictHostKeyChecking=yes "
"-oUserKnownHostsFile=\"\"\"%s\"\"\" %s "
"cloudcast@%s:"
"%s",
QuoteArgument(sdk_util_.GetScpExePath()),
quiet_ || verbosity_ < 2 ? "-q" : "", compress ? "-C" : "",
QuoteArgument(sdk_util_.GetSshConfigPath()),
QuoteArgument(sdk_util_.GetSshKeyFilePath()), ssh_port_,
sdk_util_.GetSshKnownHostsFilePath(), source_args,
QuoteArgument(gamelet_ip_), QuoteAndEscapeArgumentForSsh(dest));
start_info.name = "scp";
start_info.forward_output_to_log = forward_output_to_log_;
return process_factory_->Run(start_info);
}
absl::Status RemoteUtil::Sync(std::vector<std::string> source_filepaths,
const std::string& dest) {
absl::Status status = CheckIpPort();
if (!status.ok()) {
return status;
}
std::string source_args;
for (const std::string& sourceFilePath : source_filepaths) {
source_args += QuoteArgument(sourceFilePath) + " ";
}
ProcessStartInfo start_info;
start_info.command = absl::StrFormat(
"%s --ip=%s --port=%i -z %s %s%s",
path::Join(sdk_util_.GetDevBinPath(), "cdc_rsync"),
QuoteArgument(gamelet_ip_), ssh_port_,
quiet_ || verbosity_ < 2 ? "-q " : " ", source_args, QuoteArgument(dest));
start_info.name = "cdc_rsync";
start_info.forward_output_to_log = forward_output_to_log_;
return process_factory_->Run(start_info);
}
absl::Status RemoteUtil::Chmod(const std::string& mode,
const std::string& remote_path, bool quiet) {
std::string remote_command = absl::StrFormat(
"chmod %s %s %s", QuoteArgument(mode),
QuoteAndEscapeArgumentForSsh(remote_path), quiet ? "-f" : "");
return Run(remote_command, "chmod");
}
absl::Status RemoteUtil::Rm(const std::string& remote_path, bool force) {
std::string remote_command = absl::StrFormat(
"rm %s %s", force ? "-f" : "", QuoteAndEscapeArgumentForSsh(remote_path));
return Run(remote_command, "rm");
}
absl::Status RemoteUtil::Mv(const std::string& old_remote_path,
const std::string& new_remote_path) {
std::string remote_command =
absl::StrFormat("mv %s %s", QuoteAndEscapeArgumentForSsh(old_remote_path),
QuoteAndEscapeArgumentForSsh(new_remote_path));
return Run(remote_command, "mv");
}
absl::Status RemoteUtil::Run(std::string remote_command, std::string name) {
absl::Status status = CheckIpPort();
if (!status.ok()) {
return status;
}
ProcessStartInfo start_info =
BuildProcessStartInfoForSsh(std::move(remote_command));
start_info.name = std::move(name);
start_info.forward_output_to_log = forward_output_to_log_;
return process_factory_->Run(start_info);
}
ProcessStartInfo RemoteUtil::BuildProcessStartInfoForSsh(
std::string remote_command) {
return BuildProcessStartInfoForSshInternal("", "-- " + remote_command);
}
ProcessStartInfo RemoteUtil::BuildProcessStartInfoForSshPortForward(
int local_port, int remote_port, bool reverse) {
// (internal): Usually, one would pass in -N here, but this makes the
// connection terribly slow! As a workaround, don't use -N (will open a
// shell), but simply eat the output.
ProcessStartInfo si = BuildProcessStartInfoForSshInternal(
GetPortForwardingArg(local_port, remote_port, reverse) + "-n ", "");
si.stdout_handler = [](const void*, size_t) { return absl::OkStatus(); };
return si;
}
ProcessStartInfo RemoteUtil::BuildProcessStartInfoForSshPortForwardAndCommand(
int local_port, int remote_port, bool reverse, std::string remote_command) {
return BuildProcessStartInfoForSshInternal(
GetPortForwardingArg(local_port, remote_port, reverse),
"-- " + remote_command);
}
ProcessStartInfo RemoteUtil::BuildProcessStartInfoForSshInternal(
std::string forward_arg, std::string remote_command_arg) {
ProcessStartInfo start_info;
start_info.command = absl::StrFormat(
"%s "
"%s -tt "
"-F %s "
"-i %s "
"-oServerAliveCountMax=6 " // Number of lost msgs before ssh terminates
"-oServerAliveInterval=5 " // Time interval between alive msgs
"-oStrictHostKeyChecking=yes "
"-oUserKnownHostsFile=\"\"\"%s\"\"\" %s"
"cloudcast@%s -p %i %s",
QuoteArgument(sdk_util_.GetSshExePath()),
quiet_ || verbosity_ < 2 ? "-q" : "",
QuoteArgument(sdk_util_.GetSshConfigPath()),
QuoteArgument(sdk_util_.GetSshKeyFilePath()),
sdk_util_.GetSshKnownHostsFilePath(), forward_arg,
QuoteArgument(gamelet_ip_), ssh_port_, remote_command_arg);
start_info.forward_output_to_log = forward_output_to_log_;
return start_info;
}
absl::Status RemoteUtil::CheckIpPort() {
if (gamelet_ip_.empty() || ssh_port_ == 0) {
return MakeStatus("IP or port not set");
}
return absl::OkStatus();
}
} // namespace cdc_ft

123
common/remote_util.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 COMMON_REMOTE_UTIL_H_
#define COMMON_REMOTE_UTIL_H_
#include <string>
#include <vector>
#include "absl/status/status.h"
#include "common/process.h"
#include "common/sdk_util.h"
namespace cdc_ft {
// Utilities for executing remote commands on a gamelet through SSH.
// Windows-only.
class RemoteUtil {
public:
// If |verbosity| is > 0 and |quiet| is false, output from scp, ssh etc.
// commands is shown.
// If |quiet| is true, scp, ssh etc. commands use quiet mode.
// If |forward_output_to_log| is true, process output is forwarded to logging
// instead of this process's stdout/stderr.
RemoteUtil(int verbosity, bool quiet, ProcessFactory* process_factory,
bool forward_output_to_log);
// Returns the initialization status. Should be OK unless in case of some rare
// internal error. Should be checked before accessing any members.
const absl::Status& GetInitStatus() const {
return sdk_util_.GetInitStatus();
}
// Set IP of the remote instance and the ssh tunnel port.
void SetIpAndPort(const std::string& gamelet_ip, int ssh_port);
// Copies |source_filepaths| to the remote folder |dest| on the gamelet using
// scp. Must call either InitSsh or SetGameletIp before calling this method.
// If |compress| is true, compressed upload is used.
absl::Status Scp(std::vector<std::string> source_filepaths,
const std::string& dest, bool compress);
// Syncs |source_filepaths| to the remote folder |dest| on the gamelet using
// cdc_rsync. Must call either InitSsh or SetGameletIp before calling this
// method.
absl::Status Sync(std::vector<std::string> source_filepaths,
const std::string& dest);
// Calls 'chmod |mode| |remote_path|' on the gamelet.
// Must call either InitSsh or SetGameletIp before calling this method.
absl::Status Chmod(const std::string& mode, const std::string& remote_path,
bool quiet = false);
// Calls 'rm [-f] |remote_path|' on the gamelet.
// Must call either InitSsh or SetGameletIp before calling this method.
absl::Status Rm(const std::string& remote_path, bool force);
// Calls `mv |old_remote_path| |new_remote_path| on the gamelet.
// Must call either InitSsh or SetGameletIp before calling this method.
absl::Status Mv(const std::string& old_remote_path,
const std::string& new_remote_path);
// Runs |remote_command| on the gamelet. The command must be properly escaped.
// |name| is the name of the command displayed in the logs.
// Must call either InitSsh or SetGameletIp before calling this method.
absl::Status Run(std::string remote_command, std::string name);
// Builds an ssh command that executes |remote_command| on the gamelet.
ProcessStartInfo BuildProcessStartInfoForSsh(std::string remote_command);
// Builds an ssh command that runs SSH port forwarding to the gamelet, using
// the given |local_port| and |remote_port|.
// If |reverse| is true, sets up reverse port forwarding.
// Must call either InitSsh or SetGameletIp before calling this method.
ProcessStartInfo BuildProcessStartInfoForSshPortForward(int local_port,
int remote_port,
bool reverse);
// Builds an ssh command that executes |remote_command| on the gamelet, using
// port forwarding with given |local_port| and |remote_port|.
// If |reverse| is true, sets up reverse port forwarding.
// Must call either InitSsh or SetGameletIp before calling this method.
ProcessStartInfo BuildProcessStartInfoForSshPortForwardAndCommand(
int local_port, int remote_port, bool reverse,
std::string remote_command);
// Returns whether output is suppressed.
bool Quiet() const { return quiet_; }
private:
// Verifies that both |gamelet_ip_| and |ssh_port_| are set.
absl::Status CheckIpPort();
// Common code for BuildProcessStartInfoForSsh*.
ProcessStartInfo BuildProcessStartInfoForSshInternal(
std::string forward_arg, std::string remote_command);
const int verbosity_;
const bool quiet_;
ProcessFactory* const process_factory_;
const bool forward_output_to_log_;
SdkUtil sdk_util_;
std::string gamelet_ip_;
int ssh_port_ = 0;
};
} // namespace cdc_ft
#endif // COMMON_REMOTE_UTIL_H_

106
common/remote_util_test.cc Normal file
View File

@@ -0,0 +1,106 @@
// 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 "common/remote_util.h"
#include "absl/strings/match.h"
#include "common/log.h"
#include "gtest/gtest.h"
namespace cdc_ft {
namespace {
constexpr int kGameletPort = 12345;
constexpr char kGameletPortArg[] = "-p 12345";
constexpr char kGameletIp[] = "1.2.3.4";
constexpr char kGameletIpArg[] = "cloudcast@\"1.2.3.4\"";
constexpr int kLocalPort = 23456;
constexpr int kRemotePort = 34567;
constexpr bool kRegular = false; // Regular port forwarding
constexpr bool kReverse = true; // Reverse port forwarding
constexpr char kPortForwardingArg[] = "-L23456:localhost:34567";
constexpr char kReversePortForwardingArg[] = "-R34567:localhost:23456";
constexpr char kCommand[] = "my_command";
class RemoteUtilTest : public ::testing::Test {
public:
RemoteUtilTest()
: util_(/*verbosity=*/0, /*quiet=*/false, &process_factory_,
/*forward_output_to_log=*/true) {}
void SetUp() override {
Log::Initialize(std::make_unique<ConsoleLog>(LogLevel::kInfo));
util_.SetIpAndPort(kGameletIp, kGameletPort);
}
void TearDown() override { Log::Shutdown(); }
protected:
void ExpectContains(const std::string& str, std::vector<const char*> tokens) {
for (const char* token : tokens) {
EXPECT_TRUE(absl::StrContains(str, token))
<< str << "\ndoes not contain\n"
<< token;
}
}
WinProcessFactory process_factory_;
RemoteUtil util_;
};
TEST_F(RemoteUtilTest, BuildProcessStartInfoForSsh) {
ProcessStartInfo si = util_.BuildProcessStartInfoForSsh(kCommand);
ExpectContains(si.command,
{"ssh.exe", "GGP\\ssh\\id", "oStrictHostKeyChecking=yes",
"oUserKnownHostsFile", "known_hosts", kGameletPortArg,
kGameletIpArg, kCommand});
}
TEST_F(RemoteUtilTest, BuildProcessStartInfoForSshPortForward) {
ProcessStartInfo si = util_.BuildProcessStartInfoForSshPortForward(
kLocalPort, kRemotePort, kRegular);
ExpectContains(si.command,
{"ssh.exe", "GGP\\ssh\\id", "oStrictHostKeyChecking=yes",
"oUserKnownHostsFile", "known_hosts", kGameletPortArg,
kGameletIpArg, kPortForwardingArg});
si = util_.BuildProcessStartInfoForSshPortForward(kLocalPort, kRemotePort,
kReverse);
ExpectContains(si.command,
{"ssh.exe", "GGP\\ssh\\id", "oStrictHostKeyChecking=yes",
"oUserKnownHostsFile", "known_hosts", kGameletPortArg,
kGameletIpArg, kReversePortForwardingArg});
}
TEST_F(RemoteUtilTest, BuildProcessStartInfoForSshPortForwardAndCommand) {
ProcessStartInfo si = util_.BuildProcessStartInfoForSshPortForwardAndCommand(
kLocalPort, kRemotePort, kRegular, kCommand);
ExpectContains(si.command,
{"ssh.exe", "GGP\\ssh\\id", "oStrictHostKeyChecking=yes",
"oUserKnownHostsFile", "known_hosts", kGameletPortArg,
kGameletIpArg, kPortForwardingArg, kCommand});
si = util_.BuildProcessStartInfoForSshPortForwardAndCommand(
kLocalPort, kRemotePort, kReverse, kCommand);
ExpectContains(si.command,
{"ssh.exe", "GGP\\ssh\\id", "oStrictHostKeyChecking=yes",
"oUserKnownHostsFile", "known_hosts", kGameletPortArg,
kGameletIpArg, kReversePortForwardingArg, kCommand});
}
} // namespace
} // namespace cdc_ft

View File

@@ -0,0 +1,77 @@
// 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 "common/scoped_handle_win.h"
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <cassert>
#include <memory>
namespace cdc_ft {
ScopedHandle::ScopedHandle() : handle_(nullptr) {}
ScopedHandle::ScopedHandle(HANDLE handle) : handle_(nullptr) { Set(handle); }
ScopedHandle::ScopedHandle(ScopedHandle&& other) : handle_(nullptr) {
Set(other.Release());
}
ScopedHandle::~ScopedHandle() { Close(); }
bool ScopedHandle::IsValid() const { return IsHandleValid(handle_); }
ScopedHandle& ScopedHandle::operator=(ScopedHandle&& other) {
Set(other.Release());
return *this;
}
void ScopedHandle::Set(HANDLE handle) {
if (handle_ != handle) {
// Preserve old error code.
uint32_t last_error = GetLastError();
Close();
if (IsHandleValid(handle)) {
handle_ = handle;
}
SetLastError(last_error);
}
}
HANDLE ScopedHandle::Get() const {
assert(IsValid());
return handle_;
}
HANDLE ScopedHandle::Release() {
HANDLE temp = handle_;
handle_ = nullptr;
return temp;
}
void ScopedHandle::Close() {
if (IsHandleValid(handle_)) {
CloseHandle(handle_);
handle_ = nullptr;
}
}
bool ScopedHandle::IsHandleValid(HANDLE handle) {
return handle != nullptr && handle != INVALID_HANDLE_VALUE;
}
} // namespace cdc_ft

View File

@@ -0,0 +1,61 @@
/*
* 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 COMMON_SCOPED_HANDLE_WIN_H_
#define COMMON_SCOPED_HANDLE_WIN_H_
using HANDLE = void*;
namespace cdc_ft {
// In Windows, some functions return nullptr as invalid handles, some
// INVALID_HANDLE_VALUE. This class attempts to minimize this pain.
class ScopedHandle {
public:
ScopedHandle();
explicit ScopedHandle(HANDLE handle);
ScopedHandle(ScopedHandle&& other);
~ScopedHandle();
ScopedHandle(const ScopedHandle&) = delete;
ScopedHandle& operator=(const ScopedHandle&) = delete;
bool IsValid() const;
ScopedHandle& operator=(ScopedHandle&& other);
void Set(HANDLE handle);
HANDLE Get() const;
// Transfers ownership away from this object.
HANDLE Release();
// Explicitly closes the owned handle.
void Close();
private:
static bool IsHandleValid(HANDLE handle);
HANDLE handle_;
};
} // namespace cdc_ft
#endif // COMMON_SCOPED_HANDLE_WIN_H_

93
common/sdk_util.cc Normal file
View File

@@ -0,0 +1,93 @@
// 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 "common/sdk_util.h"
#include <cassert>
#include <string>
#include "common/clock.h"
#include "common/path.h"
#include "common/status_macros.h"
namespace cdc_ft {
SdkUtil::SdkUtil() {
init_status_ = path::GetKnownFolderPath(path::FolderId::kRoamingAppData,
&roaming_appdata_path_);
init_status_.Update(path::GetKnownFolderPath(path::FolderId::kProgramFiles,
&program_files_path_));
absl::Status status = path::GetEnv("GGP_SDK_PATH", &ggp_sdk_path_env_);
if (absl::IsNotFound(status) || ggp_sdk_path_env_.empty())
ggp_sdk_path_env_ = path::Join(program_files_path_, "GGP SDK");
// Create an empty config file if it does not exist yet.
const std::string ssh_config_path = GetSshConfigPath();
if (!path::Exists(ssh_config_path)) {
init_status_.Update(path::CreateDirRec(path::DirName(ssh_config_path)));
init_status_.Update(path::WriteFile(ssh_config_path, nullptr, 0));
}
}
SdkUtil::~SdkUtil() = default;
std::string SdkUtil::GetUserConfigPath() const {
assert(init_status_.ok());
return path::Join(roaming_appdata_path_, "GGP");
}
std::string SdkUtil::GetServicesConfigPath() const {
return path::Join(GetUserConfigPath(), "services");
}
std::string SdkUtil::GetLogPath(const char* log_base_name) const {
DefaultSystemClock clock;
std::string timestamp_ext = clock.FormatNow(".%Y%m%d-%H%M%S.log", false);
return path::Join(GetUserConfigPath(), "logs", log_base_name + timestamp_ext);
}
std::string SdkUtil::GetSshConfigPath() const {
return path::Join(GetUserConfigPath(), "ssh", "config");
}
std::string SdkUtil::GetSshKeyFilePath() const {
return path::Join(GetUserConfigPath(), "ssh", "id_rsa");
}
std::string SdkUtil::GetSshKnownHostsFilePath() const {
return path::Join(GetUserConfigPath(), "ssh", "known_hosts");
}
std::string SdkUtil::GetSDKPath() const {
assert(init_status_.ok());
return ggp_sdk_path_env_;
}
std::string SdkUtil::GetDevBinPath() const {
return path::Join(GetSDKPath(), "dev", "bin");
}
std::string SdkUtil::GetSshPath() const {
return path::Join(GetSDKPath(), "tools", "OpenSSH-Win64");
}
std::string SdkUtil::GetSshExePath() const {
return path::Join(GetSshPath(), "ssh.exe");
}
std::string SdkUtil::GetScpExePath() const {
return path::Join(GetSshPath(), "scp.exe");
}
} // namespace cdc_ft

96
common/sdk_util.h Normal file
View File

@@ -0,0 +1,96 @@
/*
* 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 COMMON_SDK_UTIL_H_
#define COMMON_SDK_UTIL_H_
#include <string>
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "common/platform.h"
#if !PLATFORM_WINDOWS
#error SdkUtil only supports Windows so far.
#endif
namespace cdc_ft {
// Provides paths to selected files in the Stadia Windows SDK.
class SdkUtil {
public:
SdkUtil();
~SdkUtil();
// Returns the initialization status. Should be OK unless in case of some rare
// internal error. Should be checked before accessing any members.
const absl::Status& GetInitStatus() const { return init_status_; }
// Returns the path of the SDK user configuration, e.g.
// %APPDATA%\GGP.
std::string GetUserConfigPath() const;
// Returns the path of the SDK services configuration, e.g.
// %APPDATA%\GGP\services.
std::string GetServicesConfigPath() const;
// Returns the path of a log file with given |log_base_name|, e.g.
// %APPDATA%\GGP\logs\log_base_name.20210729-125930.log.
std::string GetLogPath(const char* log_base_name) const;
// Returns the path of the ssh configuration file, e.g.
// %APPDATA%\GGP\ssh\config.
std::string GetSshConfigPath() const;
// Returns the path of the ssh private key file in the SDK configuration, e.g.
// %APPDATA%\GGP\ssh\id_rsa.
std::string GetSshKeyFilePath() const;
// Returns the path of the ssh known_hosts file in the SDK configuration, e.g.
// %APPDATA%\GGP\ssh\known_hosts.
std::string GetSshKnownHostsFilePath() const;
// Returns the path of the installed SDK, e.g.
// C:\Program Files\GGP SDK.
std::string GetSDKPath() const;
// Returns the path of the dev tools that ship with the SDK, e.g.
// C:\Program Files\GGP SDK\dev\bin.
std::string GetDevBinPath() const;
// Returns the path of the OpenSSH tools that ship with the SDK, e.g.
// C:\Program Files\GGP SDK\tools\OpenSSH-Win64.
std::string GetSshPath() const;
// Returns the path of ssh.exe that ships with the SDK, e.g.
// C:\Program Files\GGP SDK\tools\OpenSSH-Win64\ssh.exe.
std::string GetSshExePath() const;
// Returns the path of scp.exe that ships with the SDK, e.g.
// C:\Program Files\GGP SDK\tools\OpenSSH-Win64\scp.exe.
std::string GetScpExePath() const;
private:
std::string roaming_appdata_path_;
std::string program_files_path_;
std::string ggp_sdk_path_env_;
absl::Status init_status_;
std::string full_sdk_version_;
};
} // namespace cdc_ft
#endif // COMMON_SDK_UTIL_H_

115
common/sdk_util_test.cc Normal file
View File

@@ -0,0 +1,115 @@
// 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 "common/sdk_util.h"
#include <stdlib.h>
#include "common/log.h"
#include "common/path.h"
#include "common/status_test_macros.h"
#include "gtest/gtest.h"
namespace cdc_ft {
namespace {
constexpr bool kCreateFile = true;
constexpr bool kDontCreateFile = false;
class SdkUtilTest : public ::testing::Test {
public:
void SetUp() override {
Log::Initialize(std::make_unique<ConsoleLog>(LogLevel::kInfo));
}
void TearDown() override {
Log::Shutdown();
for (std::string dir_path : test_created_directories_) {
EXPECT_OK(path::RemoveDirRec(dir_path));
}
}
protected:
void CheckSdkPaths(const SdkUtil& sdk_util, const std::string& sdk_dir) {
EXPECT_EQ(sdk_util.GetSDKPath(), sdk_dir);
EXPECT_EQ(sdk_util.GetSshPath(),
path::Join(sdk_dir, "tools\\OpenSSH-Win64"));
EXPECT_EQ(sdk_util.GetSshExePath(),
path::Join(sdk_dir, "tools\\OpenSSH-Win64\\ssh.exe"));
EXPECT_EQ(sdk_util.GetScpExePath(),
path::Join(sdk_dir, "tools\\OpenSSH-Win64\\scp.exe"));
EXPECT_EQ(sdk_util.GetDevBinPath(), path::Join(sdk_dir, "dev", "bin"));
}
void SetupGetSdkVersion(const std::string& file_content,
bool create_version_file) {
std::string ggp_sdk_path = std::tmpnam(nullptr);
EXPECT_OK(path::SetEnv("GGP_SDK_PATH", ggp_sdk_path));
test_created_directories_.push_back(ggp_sdk_path);
EXPECT_OK(path::CreateDirRec(ggp_sdk_path));
if (create_version_file) {
std::string version_path = path::Join(ggp_sdk_path, "VERSION");
EXPECT_OK(path::WriteFile(version_path, file_content.c_str(),
file_content.size()));
}
}
// Contains assets which where created during test.
// The assets are to be deleted in the end of the test.
std::vector<std::string> test_created_directories_;
};
TEST_F(SdkUtilTest, CheckRoamingAppDataPaths) {
SdkUtil sdk_util;
EXPECT_OK(sdk_util.GetInitStatus());
std::string appdata_dir;
EXPECT_OK(
path::GetKnownFolderPath(path::FolderId::kRoamingAppData, &appdata_dir));
const std::string ggp_path = path::Join(appdata_dir, "GGP");
EXPECT_EQ(sdk_util.GetUserConfigPath(), ggp_path);
EXPECT_EQ(sdk_util.GetServicesConfigPath(), path::Join(ggp_path, "services"));
EXPECT_EQ(sdk_util.GetSshConfigPath(), path::Join(ggp_path, "ssh", "config"));
EXPECT_EQ(sdk_util.GetSshKeyFilePath(),
path::Join(ggp_path, "ssh", "id_rsa"));
EXPECT_EQ(sdk_util.GetSshKnownHostsFilePath(),
path::Join(ggp_path, "ssh", "known_hosts"));
}
TEST_F(SdkUtilTest, CheckSdkPathsWithoutGgpSdkPathEnv) {
// Clear environment variable and figure out default SDK dir.
EXPECT_OK(path::SetEnv("GGP_SDK_PATH", ""));
std::string program_files_dir;
EXPECT_OK(path::GetKnownFolderPath(path::FolderId::kProgramFiles,
&program_files_dir));
const std::string sdk_dir = path::Join(program_files_dir, "GGP SDK");
SdkUtil sdk_util;
EXPECT_OK(sdk_util.GetInitStatus());
CheckSdkPaths(sdk_util, sdk_dir);
}
TEST_F(SdkUtilTest, CheckSdkPathsWithGgpSdkPathEnv) {
// Set a path with unicode character.
std::string sdk_dir = u8"C:\\I\\\\GGP SDK\\";
EXPECT_OK(path::SetEnv("GGP_SDK_PATH", sdk_dir));
SdkUtil sdk_util;
EXPECT_OK(sdk_util.GetInitStatus());
CheckSdkPaths(sdk_util, sdk_dir);
}
} // namespace
} // namespace cdc_ft

35
common/semaphore.cc Normal file
View File

@@ -0,0 +1,35 @@
// 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 "common/semaphore.h"
namespace cdc_ft {
Semaphore::Semaphore(int initial_count) : count_(initial_count) {}
Semaphore::~Semaphore() = default;
void Semaphore::Wait() {
std::unique_lock<std::mutex> lock(mutex_);
condition_.wait(lock, [&]() { return count_ > 0; });
count_--;
}
void Semaphore::Signal() {
std::lock_guard<std::mutex> lock(mutex_);
count_++;
condition_.notify_one();
}
} // namespace cdc_ft

52
common/semaphore.h Normal file
View File

@@ -0,0 +1,52 @@
/*
* 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 COMMON_SEMAPHORE_H_
#define COMMON_SEMAPHORE_H_
#include <condition_variable>
#include <mutex>
namespace cdc_ft {
// Standard semaphore. Can be replaced by std::counting_semaphore in C++20.
// Not fair! Wait() does not unlock in FIFO order.
class Semaphore {
public:
// Initialize the semaphore with |initial_count|. The count indicates how
// often Wait() can be called without blocking. There is no max count.
explicit Semaphore(int initial_count);
~Semaphore();
// If the count is larger than zero, decreases the count and returns
// immediately. If the count is zero, blocks until some other thread calls
// Signal() and decreases the count.
// This method is thread-safe.
void Wait();
// Increases the count.
// This method is thread-safe.
void Signal();
private:
uint32_t count_;
std::mutex mutex_;
std::condition_variable condition_;
};
} // namespace cdc_ft
#endif // COMMON_SEMAPHORE_H_

122
common/semaphore_test.cc Normal file
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 "common/semaphore.h"
#include <atomic>
#include <thread>
#include <vector>
#include "common/util.h"
#include "gtest/gtest.h"
namespace cdc_ft {
namespace {
class SemaphoreTest : public ::testing::Test {
protected:
bool PollUntil(std::function<bool()> predicate, uint32_t timeout_ms) {
uint32_t ms = 0;
while (!predicate()) {
Util::Sleep(1);
ms++;
if (ms >= timeout_ms) {
return false;
}
}
return true;
}
};
TEST_F(SemaphoreTest, BlocksIfInitialCountIsZero) {
Semaphore semaphore(0);
std::atomic_bool ready(false);
std::atomic_bool done(false);
std::thread thread([&semaphore, &ready, &done]() {
ready = true;
semaphore.Wait();
done = true;
});
EXPECT_TRUE(PollUntil([&ready]() { return ready.load(); }, 5000));
// This will time out, so use a short timeout. Is there a way to test whether
// the thread is blocking without using a timeout?
EXPECT_FALSE(PollUntil([&done]() { return done.load(); }, 10));
semaphore.Signal();
EXPECT_TRUE(PollUntil([&done]() { return done.load(); }, 5000));
thread.join();
}
TEST_F(SemaphoreTest, DoesNotBlockIfInitialCountIsOne) {
Semaphore semaphore(1);
std::atomic_bool first_wait(false);
std::atomic_bool done(false);
std::thread thread([&semaphore, &first_wait, &done]() {
semaphore.Wait();
first_wait = true;
semaphore.Wait();
done = true;
});
EXPECT_TRUE(PollUntil([&first_wait]() { return first_wait.load(); }, 5000));
// This will time out, so use a short timeout. Is there a way to test whether
// the thread is blocking without using a timeout?
EXPECT_FALSE(PollUntil([&done]() { return done.load(); }, 10));
semaphore.Signal();
EXPECT_TRUE(PollUntil([&done]() { return done.load(); }, 5000));
thread.join();
}
TEST_F(SemaphoreTest, SignalManyThreads) {
Semaphore semaphore(0);
std::atomic_int a(0);
std::atomic_int b(0);
std::atomic_int c(0);
const int N = 16;
std::vector<std::thread> threads;
for (int n = 0; n < N; ++n) {
threads.emplace_back([&semaphore, &a, &b, &c]() {
++a;
semaphore.Wait();
++b;
semaphore.Wait();
++c;
});
}
// All threads should be at the first wait.
EXPECT_TRUE(PollUntil([&]() { return a == N; }, 5000));
for (int n = 0; n < N; ++n) {
semaphore.Signal();
}
// Some threads should be at or past the second wait.
// Note: If the Semaphore were fair, it would be cnt[1] == N and cnt[2] == 0.
EXPECT_TRUE(PollUntil([&]() { return b + c == N; }, 5000));
for (int n = 0; n < N; ++n) {
semaphore.Signal();
}
// All threads should have finished.
EXPECT_TRUE(PollUntil([&]() { return b == N && c == N; }, 5000));
for (int n = 0; n < N; ++n) {
threads[n].join();
}
}
} // namespace
} // namespace cdc_ft

158
common/stats_collector.cc Normal file
View File

@@ -0,0 +1,158 @@
// 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 "common/stats_collector.h"
#include <map>
#include <thread>
#include "absl/strings/str_format.h"
#include "absl/synchronization/mutex.h"
#include "common/log.h"
#include "common/stopwatch.h"
namespace cdc_ft {
namespace {
constexpr absl::Duration kStatsPrintPeriod = absl::Seconds(1);
}
class StatsCollectorImpl : public StatsCollector {
public:
class DurationScopeImpl : public DurationScope {
public:
DurationScopeImpl(StatsCollectorImpl* stats_collector, const char* name)
: stats_collector_(stats_collector), name_(name) {
assert(stats_collector_);
}
~DurationScopeImpl() override {
stats_collector_->DoRecordDuration(name_, stopwatch_.Elapsed());
}
private:
StatsCollectorImpl* stats_collector_;
const char* name_;
Stopwatch stopwatch_;
};
StatsCollectorImpl() {
print_thread_ = std::thread([this]() { ThreadPrintMain(); });
}
~StatsCollectorImpl() override {
{
absl::MutexLock lock(&mutex_);
shutdown_ = true;
}
if (print_thread_.joinable()) {
print_thread_.join();
}
}
void IncCounter(const char* name, size_t value) override
ABSL_LOCKS_EXCLUDED(mutex_) {
absl::MutexLock lock(&mutex_);
counters_[name] += value;
}
std::unique_ptr<DurationScope> RecordDuration(const char* name) override {
return std::make_unique<DurationScopeImpl>(this, name);
}
private:
void DoRecordDuration(const char* name, absl::Duration duration)
ABSL_LOCKS_EXCLUDED(mutex_) {
absl::MutexLock lock(&mutex_);
durations_[std::move(name)] += duration;
}
void ThreadPrintMain() ABSL_LOCKS_EXCLUDED(mutex_) {
absl::MutexLock lock(&mutex_);
while (!shutdown_) {
Stopwatch sw;
auto cond = [&]() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_) {
return shutdown_ || sw.Elapsed() >= kStatsPrintPeriod;
};
mutex_.Await(absl::Condition(&cond));
if (shutdown_) {
break;
}
if (counters_.empty() && durations_.empty()) continue;
std::string line;
for (const auto& [name, value] : counters_) {
line += absl::StrFormat("%s:%u ", name, value);
}
for (const auto& [name, value] : durations_) {
line +=
absl::StrFormat("%s:%0.3fs ", name, absl::ToDoubleSeconds(value));
}
counters_.clear();
durations_.clear();
LOG_WARNING("\n%s\n", line);
}
}
absl::Mutex mutex_;
std::map<std::string, size_t> counters_ ABSL_GUARDED_BY(mutex_);
std::map<std::string, absl::Duration> durations_ ABSL_GUARDED_BY(mutex_);
std::thread print_thread_;
bool shutdown_ ABSL_GUARDED_BY(mutex_) = false;
};
class NullStatsCollector : public StatsCollector {
public:
class NullDurationScope : public DurationScope {};
NullStatsCollector() = default;
~NullStatsCollector() override = default;
void IncCounter(const char*, size_t) override {}
std::unique_ptr<DurationScope> RecordDuration(const char*) override {
return std::make_unique<NullDurationScope>();
}
};
StatsCollector::StatsCollector() = default;
StatsCollector::~StatsCollector() = default;
StatsCollector* StatsCollector::instance_ = nullptr;
StatsCollector::DurationScope::~DurationScope() = default;
// static
void StatsCollector::Initialize() {
assert(!instance_);
instance_ = new StatsCollectorImpl;
}
// static
void StatsCollector::Shutdown() {
assert(instance_);
delete instance_;
instance_ = nullptr;
}
// static
StatsCollector* StatsCollector::Instance() {
static NullStatsCollector null_instance;
return instance_ ? instance_ : &null_instance;
}
} // namespace cdc_ft

54
common/stats_collector.h Normal file
View File

@@ -0,0 +1,54 @@
/*
* 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 COMMON_STATS_COLLECTOR_H_
#define COMMON_STATS_COLLECTOR_H_
#include <memory>
namespace cdc_ft {
class StatsCollector {
public:
StatsCollector();
virtual ~StatsCollector();
// Initializes the stats collector. If not called, the instance returned from
// Instance() is a no-op stats collector where all calls have no effect.
static void Initialize();
// Shuts down the stats collector. Should be called during shutdown if
// Initialize() was called during startup.
static void Shutdown();
// Returns a no-op stats collector if Initialize() has not been called.
static StatsCollector* Instance();
class DurationScope {
public:
virtual ~DurationScope();
};
virtual void IncCounter(const char* name, size_t value = 1) = 0;
virtual std::unique_ptr<DurationScope> RecordDuration(const char* name) = 0;
private:
static StatsCollector* instance_;
};
} // namespace cdc_ft
#endif // COMMON_STATS_COLLECTOR_H_

54
common/status.cc Normal file
View File

@@ -0,0 +1,54 @@
// 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 "common/status.h"
#include "absl/strings/numbers.h"
namespace cdc_ft {
namespace {
constexpr char kTagKey[] = "tag";
absl::Cord TagToCord(Tag tag) {
return absl::Cord(std::to_string(static_cast<int>(tag)));
}
absl::optional<Tag> CordToTag(absl::optional<absl::Cord> cord) {
if (!cord.has_value()) return {};
int value;
absl::numbers_internal::safe_strto32_base(std::string(cord.value()), &value,
10);
return value >= 0 && value < static_cast<int>(Tag::kCount)
? static_cast<Tag>(value)
: absl::optional<Tag>();
}
} // namespace
absl::Status SetTag(absl::Status status, Tag tag) {
status.SetPayload(kTagKey, TagToCord(tag));
return status;
}
bool HasTag(const absl::Status& status, Tag tag) {
return status.GetPayload(kTagKey) == TagToCord(tag);
}
absl::optional<Tag> GetTag(const absl::Status& status) {
return CordToTag(status.GetPayload(kTagKey));
}
} // namespace cdc_ft

101
common/status.h Normal file
View File

@@ -0,0 +1,101 @@
/*
* 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 COMMON_STATUS_H_
#define COMMON_STATUS_H_
#include "absl/status/status.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "common/platform.h"
namespace cdc_ft {
//
// Convenience helper functions for status creation.
//
inline absl::Status MakeStatus(const char* message) {
return absl::InternalError(message);
}
template <typename... Args>
absl::Status MakeStatus(const absl::FormatSpec<Args...>& format, Args... args) {
return absl::InternalError(absl::StrFormat(format, args...));
}
// Convenience helper for cases that may or may not have a message to wrap.
template <typename... Args>
absl::Status WrapStatus(absl::Status inner_status) {
return inner_status;
}
// Returns OK if |inner_status| is OK and a copy of |inner_status| with given
// message + |inner_status|'s message otherwise.
template <typename... Args>
absl::Status WrapStatus(absl::Status inner_status,
const absl::FormatSpec<Args...>& format, Args... args) {
if (inner_status.ok()) {
return inner_status;
}
std::string message = absl::StrFormat(format, args...);
absl::Status wrapped_status(
inner_status.code(), absl::StrCat(inner_status.message(), "; ", message));
inner_status.ForEachPayload(
[&wrapped_status](absl::string_view key, const absl::Cord& value) {
wrapped_status.SetPayload(key, value);
});
return wrapped_status;
}
//
// Tags attached to absl::Status to allow more fine-grained error handling.
//
enum class Tag : uint8_t {
// Sending end of the socket was closed, so receiving end can no longer read.
kSocketEof = 0,
// Bind failed, probably because there's another instance of cdc_rsync
// running.
kAddressInUse = 1,
// The gamelet components need to be re-deployed.
kDeployServer = 2,
// Something asks for user input, but we're in quiet mode.
kInstancePickerNotAvailableInQuietMode = 3,
// Timeout while trying to connect to the gamelet component.
kConnectionTimeout = 4,
// MUST BE LAST.
kCount = 5,
};
// Tags a status. No-op if |status| is OK. Overwrites existing tags.
absl::Status SetTag(absl::Status status, Tag tag);
// Returns the associated tag. Empty if none.
absl::optional<Tag> GetTag(const absl::Status& status);
// Checks whether the status has a certain tag.
bool HasTag(const absl::Status& status, Tag tag);
} // namespace cdc_ft
#endif // COMMON_STATUS_H_

42
common/status_macros.h Normal file
View File

@@ -0,0 +1,42 @@
/*
* 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 COMMON_STATUS_MACROS_H_
#define COMMON_STATUS_MACROS_H_
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_format.h"
#include "common/status.h"
#define RETURN_IF_ERROR(expr, ...) \
do { \
const absl::Status __status__ = (expr); \
if (!__status__.ok()) { \
return ::cdc_ft::WrapStatus(__status__, ##__VA_ARGS__); \
} \
} while (0)
#define ASSIGN_OR_RETURN(lhs, expr, ...) \
do { \
auto __status__ = (expr); \
if (!__status__.ok()) { \
return ::cdc_ft::WrapStatus(__status__.status(), ##__VA_ARGS__); \
} \
lhs = std::move(__status__.value()); \
} while (0)
#endif // COMMON_STATUS_MACROS_H_

View File

@@ -0,0 +1,82 @@
/*
* 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 COMMON_STATUS_TEST_MACROS_H_
#define COMMON_STATUS_TEST_MACROS_H_
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "common/platform.h"
#ifndef PLATFORM_WINDOWS
#define __forceinline inline __attribute__((always_inline))
#endif
namespace internal {
namespace status_test_macros {
// Helper functions to get status from both status and StatusOr.
template <class T>
const absl::Status& ToStatus(const absl::StatusOr<T>& result) {
return result.status();
}
__forceinline const absl::Status& ToStatus(const absl::Status& status) {
return status;
}
} // namespace status_test_macros
} // namespace internal
// Little convenience macros to check the return value of an absl::Status in
// gTest unit tests.
#define EXPECT_OK(x) VERIFY_OK(EXPECT, x)
#define ASSERT_OK(x) VERIFY_OK(ASSERT, x)
#define VERIFY_OK(action, x) \
do { \
auto __status = ::internal::status_test_macros::ToStatus(x); \
action##_TRUE(__status.ok()) << "Error: " << __status.ToString(); \
} while (0)
#define EXPECT_NOT_OK(x) VERIFY_NOT_OK(EXPECT, x)
#define ASSERT_NOT_OK(x) VERIFY_NOT_OK(ASSERT, x)
#define VERIFY_NOT_OK(action, x) action##_FALSE((x).ok())
// type should be one of the absl::IsXXX errors, e.g. "Canceled".
#define EXPECT_ERROR(type, x) VERIFY_ERROR(EXPECT, type, x)
#define ASSERT_ERROR(type, x) VERIFY_ERROR(ASSERT, type, x)
#define VERIFY_ERROR(action, type, x) \
do { \
auto __status = ::internal::status_test_macros::ToStatus(x); \
action##_TRUE(absl::Is##type(__status)) \
<< "Unexpected status '" << __status.ToString() << "', expected " \
<< #type; \
} while (0)
// type should be one of the absl::IsXXX errors, e.g. "Canceled".
#define EXPECT_ERROR_MSG(type, msg, x) VERIFY_ERROR_MSG(EXPECT, type, msg, x)
#define ASSERT_ERROR_MSG(type, msg, x) VERIFY_ERROR_MSG(ASSERT, type, msg, x)
#define VERIFY_ERROR_MSG(action, type, msg, x) \
do { \
auto __status = ::internal::status_test_macros::ToStatus(x); \
action##_TRUE(absl::Is##type(__status)) \
<< "Unexpected status '" << __status.ToString() << "', expected " \
<< #type; \
action##_NE(__status.message().find(msg), std::string::npos) \
<< "Unexpected message '" << __status.message() << "', expected " \
<< "'" << msg << "'"; \
} while (0)
#endif // COMMON_STATUS_TEST_MACROS_H_

35
common/stopwatch.cc Normal file
View File

@@ -0,0 +1,35 @@
// 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 "common/stopwatch.h"
namespace cdc_ft {
Stopwatch::Stopwatch(SteadyClock* clock) : clock_(clock) { Reset(); }
Stopwatch::~Stopwatch() = default;
double Stopwatch::ElapsedSeconds() const {
// Assigning to a duration<double> automagically converts to seconds.
const std::chrono::duration<double> elapsed = clock_->Now() - start_;
return elapsed.count();
}
void Stopwatch::Reset() { start_ = clock_->Now(); }
absl::Duration Stopwatch::Elapsed() const {
return absl::FromChrono(clock_->Now() - start_);
}
} // namespace cdc_ft

52
common/stopwatch.h Normal file
View File

@@ -0,0 +1,52 @@
/*
* 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 COMMON_STOPWATCH_H_
#define COMMON_STOPWATCH_H_
#include <chrono>
#include "absl/time/time.h"
#include "common/clock.h"
namespace cdc_ft {
class Stopwatch {
public:
using Timestamp = SteadyClock::Timestamp;
explicit Stopwatch(SteadyClock* clock = DefaultSteadyClock::GetInstance());
~Stopwatch();
// Returns the elapsed time (in seconds) relative to construction or to the
// last time when Reset() was called.
double ElapsedSeconds() const;
// Resets the elapsed time to zero.
void Reset();
// Returns the duration relative to construction or to the last time when
// Reset() was called.
absl::Duration Elapsed() const;
private:
SteadyClock* clock_;
Timestamp start_;
};
} // namespace cdc_ft
#endif // COMMON_STOPWATCH_H_

47
common/stopwatch_test.cc Normal file
View File

@@ -0,0 +1,47 @@
// 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 "common/stopwatch.h"
#include "common/testing_clock.h"
#include "gtest/gtest.h"
namespace cdc_ft {
namespace {
class StopwatchTest : public ::testing::Test {};
TEST_F(StopwatchTest, DefaultStopwatch) {
// Cover DefaultClock.
Stopwatch stopwatch;
EXPECT_GE(stopwatch.ElapsedSeconds(), 0.0);
}
TEST_F(StopwatchTest, Stopwatch) {
TestingSteadyClock clock;
Stopwatch stopwatch(&clock);
EXPECT_EQ(stopwatch.ElapsedSeconds(), 0.0);
clock.Advance(1000);
EXPECT_EQ(stopwatch.ElapsedSeconds(), 1.0);
clock.Advance(500);
EXPECT_EQ(stopwatch.ElapsedSeconds(), 1.5);
stopwatch.Reset();
EXPECT_EQ(stopwatch.ElapsedSeconds(), 0.0);
clock.Advance(250);
EXPECT_EQ(stopwatch.ElapsedSeconds(), 0.25);
}
} // namespace
} // namespace cdc_ft

158
common/stub_process.cc Normal file
View File

@@ -0,0 +1,158 @@
// 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 "common/stub_process.h"
#include "absl/strings/match.h"
#include "absl/strings/str_format.h"
#include "common/status_macros.h"
#include "common/util.h"
namespace cdc_ft {
class StubProcess : public Process {
public:
StubProcess(const ProcessStartInfo& start_info, std::string std_out,
std::string std_err, uint32_t exit_code, bool has_exited,
bool never_exits, StubProcessFactory::ProcessHandlerFn handler)
: Process(start_info),
std_out_(std::move(std_out)),
std_err_(std::move(std_err)),
exit_code_(exit_code),
has_exited_(has_exited),
never_exits_(never_exits),
handler_(handler) {}
~StubProcess() = default;
// Process:
absl::Status Start() override { return absl::OkStatus(); }
absl::Status RunUntil(std::function<bool()> exit_condition) override {
if (handler_) {
handler_(&std_out_, &std_err_, &exit_code_);
}
// Write stdout/stderr including the null terminator.
if (start_info_.stdout_handler) {
RETURN_IF_ERROR(
start_info_.stdout_handler(std_out_.c_str(), std_out_.size() + 1));
}
if (start_info_.stderr_handler) {
RETURN_IF_ERROR(
start_info_.stderr_handler(std_out_.c_str(), std_err_.size() + 1));
}
if (never_exits_) {
// Poll until exit condition (e.g. timeout) is satisfied.
while (!exit_condition()) Util::Sleep(1);
} else {
// Exit immediately.
has_exited_ = true;
}
return absl::OkStatus();
}
absl::Status Terminate() override { return absl::OkStatus(); }
absl::Status WriteToStdIn(const void*, size_t) override {
return absl::OkStatus();
}
void CloseStdIn() override {}
bool HasExited() const override { return has_exited_; }
uint32_t ExitCode() const override { return exit_code_; }
absl::Status GetStatus() const override { return absl::OkStatus(); }
private:
std::string std_out_;
std::string std_err_;
uint32_t exit_code_;
const bool never_exits_;
bool has_exited_ = false;
StubProcessFactory::ProcessHandlerFn handler_;
};
// An ErrorProcess is returned if the corresponding command isn't handled in
// StubProcessFactory::Create(). It will fail to start.
class ErrorProcess : public Process {
public:
explicit ErrorProcess(const ProcessStartInfo& start_info)
: Process(start_info),
status_(absl::InternalError(
absl::StrFormat("Unhandled process '%s'", start_info_.command))) {}
~ErrorProcess() = default;
// Process:
absl::Status Start() override { return status_; }
absl::Status RunUntil(std::function<bool()>) override { return status_; }
absl::Status Terminate() override { return status_; }
absl::Status WriteToStdIn(const void*, size_t) override { return status_; }
void CloseStdIn() override {}
bool HasExited() const override { return false; }
uint32_t ExitCode() const override { return kExitCodeStillRunning; }
absl::Status GetStatus() const override { return status_; }
private:
absl::Status status_;
};
StubProcessFactory::~StubProcessFactory() = default;
std::unique_ptr<Process> StubProcessFactory::Create(
const ProcessStartInfo& start_info) {
for (const ProcessInfo& pi : process_info_) {
if (absl::StrContains(start_info.command, pi.command_part)) {
return std::make_unique<StubProcess>(start_info, pi.std_out, pi.std_err,
pi.exit_code, pi.has_exited,
pi.never_exits, pi.handler);
}
}
return std::make_unique<ErrorProcess>(start_info);
}
void StubProcessFactory::SetProcessOutput(std::string command_part,
std::string std_out,
std::string std_err,
uint32_t exit_code) {
process_info_.push_back({std::move(command_part), std::move(std_out),
std::move(std_err), exit_code,
/*has_exited=*/false, /*never_exits=*/false,
ProcessHandlerFn()});
}
void StubProcessFactory::SetProcessNeverExits(std::string command_part) {
process_info_.push_back({std::move(command_part), "", "", 1,
/*has_exited=*/false,
/*never_exits=*/true, ProcessHandlerFn()});
}
void StubProcessFactory::SetProcessExitsImmediately(std::string command_part,
uint32_t exit_code) {
process_info_.push_back({std::move(command_part), /*std_out=*/"",
/*std_err=*/"", exit_code, /*has_exited=*/true,
/*never_exits=*/false, ProcessHandlerFn()});
}
void StubProcessFactory::SetProcessHandler(std::string command_part,
ProcessHandlerFn handler) {
process_info_.push_back({std::move(command_part), /*std_out=*/"",
/*std_err=*/"",
/*exit_code_=*/0, /*has_exited=*/false,
/*never_exits=*/false, handler});
}
} // namespace cdc_ft

78
common/stub_process.h Normal file
View File

@@ -0,0 +1,78 @@
/*
* 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 COMMON_STUB_PROCESS_H_
#define COMMON_STUB_PROCESS_H_
#include <vector>
#include "absl/status/status.h"
#include "common/process.h"
namespace cdc_ft {
// A stub process returns prefabricated stdout, stderr and exit codes to
// predetermined commands.
class StubProcessFactory : public ProcessFactory {
public:
using ProcessHandlerFn = std::function<void(
std::string* std_out, std::string* std_err, uint32_t* exit_code)>;
virtual StubProcessFactory::~StubProcessFactory();
// ProcessFactory:
std::unique_ptr<Process> Create(const ProcessStartInfo& start_info) override;
// Sets up a stub process that is returned by a call to Create() if the
// command contains |command_part|. The stub process exits on RunUntil(),
// writes the given |std_out| and |std_err| to the output stream and exits
// with |exit_code|.
void SetProcessOutput(std::string command_part, std::string std_out,
std::string std_err, uint32_t exit_code);
// Sets up a stub process that is returned by a call to Create() if the
// command contains |command_part|. The process never exits. Useful for
// testing timeout conditions.
void SetProcessNeverExits(std::string command_part);
// Sets up a stub process that is returned by a call to Create() if the
// command contains |command_part|. The stub process is immediately in an
// exited state with code |exit_code| and does not write stdout or stderr.
void SetProcessExitsImmediately(std::string command_part, uint32_t exit_code);
// Sets up a stub process that is returned by a call to Create() if the
// command contains |command_part|. The stub process calls |handler| on
// RunUntil(), writes returned |std_out| and |std_err| to the output stream
// and exits with |exit_code|.
void SetProcessHandler(std::string command_part, ProcessHandlerFn handler);
private:
struct ProcessInfo {
std::string command_part;
std::string std_out;
std::string std_err;
uint32_t exit_code;
bool has_exited;
bool never_exits;
ProcessHandlerFn handler;
};
std::vector<ProcessInfo> process_info_;
};
} // namespace cdc_ft
#endif // COMMON_STUB_PROCESS_H_

53
common/test_main.cc Normal file
View File

@@ -0,0 +1,53 @@
// 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 "common/path.h"
#include "gtest/gtest.h"
#include "tools/cpp/runfiles/runfiles.h"
using bazel::tools::cpp::runfiles::Runfiles;
namespace cdc_ft {
namespace {
Runfiles* runfiles_ptr;
}
std::string GetTestDataDir(const char* test_data_dir) {
std::string test_path =
::testing::UnitTest::GetInstance()->current_test_info()->file();
std::string test_dir = path::DirName(test_path);
std::string root_runfile = path::ToUnix(
path::Join("cdc_file_transfer", test_dir, "testdata", "root.txt"));
assert(runfiles_ptr != nullptr);
std::string root_path = runfiles_ptr->Rlocation(root_runfile);
path::FixPathSeparators(&root_path);
EXPECT_TRUE(path::Exists(root_path))
<< path::Join(test_dir, "testdata") << " directory has no root.txt file";
return path::Join(path::DirName(root_path), test_data_dir);
}
} // namespace cdc_ft
int main(int argc, char** argv) {
std::string error;
std::unique_ptr<Runfiles> runfiles(Runfiles::Create(argv[0], &error));
if (runfiles == nullptr) {
std::cerr << "Failed to locate runtime files: " << error << std::endl;
return 1;
}
cdc_ft::runfiles_ptr = runfiles.get();
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

32
common/test_main.h Normal file
View File

@@ -0,0 +1,32 @@
/*
* 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 COMMON_TEST_MAIN_H_
#define COMMON_TEST_MAIN_H_
#include <string>
namespace cdc_ft {
// Retreives the full path of the testdata/|test_data_dir| directory from
// runfiles. The testdata directory must have an empty file named "root.txt".
// |test_data_dir| usually matches the filename of the test file without the
// trailing "_test.cc", e.g. "path" for "path_test.cc".
std::string GetTestDataDir(const char* test_data_dir);
} // namespace cdc_ft
#endif // COMMON_TEST_MAIN_H_

View File

View File

0
common/testdata/dir_iter/a/aa1.txt vendored Normal file
View File

0
common/testdata/dir_iter/a/aa2.txt vendored Normal file
View File

View File

View File

View File

View File

View File

View File

0
common/testdata/dir_iter/c/c1.txt vendored Normal file
View File

0
common/testdata/dir_iter/c/c2.txt vendored Normal file
View File

0
common/testdata/dir_iter/d/d1.txt vendored Normal file
View File

0
common/testdata/dir_iter/d/d2.txt vendored Normal file
View File

0
common/testdata/dir_iter/root.txt vendored Normal file
View File

View File

@@ -0,0 +1 @@
changed fake cdc_rsync_server

View File

@@ -0,0 +1 @@
fake cdc_rsync_server

0
common/testdata/root.txt vendored Normal file
View File

44
common/testing_clock.cc Normal file
View File

@@ -0,0 +1,44 @@
// 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 "common/testing_clock.h"
#include "common/stopwatch.h"
namespace cdc_ft {
TestingSteadyClock::Timestamp TestingSteadyClock::Now() const {
const Timestamp prev_now = now_;
now_ += std::chrono::milliseconds(auto_advance_ms_);
return prev_now;
}
void TestingSteadyClock::Advance(int milliseconds) {
now_ += std::chrono::milliseconds(milliseconds);
}
void TestingSteadyClock::AutoAdvance(int milliseconds) {
auto_advance_ms_ = milliseconds;
}
TestingSystemClock::TestingSystemClock()
: now_(std::chrono::system_clock::now()) {}
TestingSystemClock::Timestamp TestingSystemClock::Now() const { return now_; }
void TestingSystemClock::Advance(int milliseconds) {
now_ += std::chrono::milliseconds(milliseconds);
}
} // namespace cdc_ft

56
common/testing_clock.h Normal file
View File

@@ -0,0 +1,56 @@
/*
* 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 COMMON_TESTING_CLOCK_H_
#define COMMON_TESTING_CLOCK_H_
#include "common/clock.h"
namespace cdc_ft {
class TestingSteadyClock : public SteadyClock {
public:
// SteadyClock:
Timestamp Now() const override;
// Explicitly advances the timestamp by the given number of |milliseconds|.
void Advance(int milliseconds);
// Sets a time that is added to the current time after every Now() call.
void AutoAdvance(int milliseconds);
private:
mutable Timestamp now_;
int auto_advance_ms_ = 0;
};
class TestingSystemClock : public SystemClock {
public:
TestingSystemClock();
// SystemClock:
Timestamp Now() const override;
// Explicitly advances the timestamp by the given number of |milliseconds|.
void Advance(int milliseconds);
private:
Timestamp now_;
};
} // namespace cdc_ft
#endif // COMMON_TESTING_CLOCK_H_

52
common/thread_safe_map.h Normal file
View File

@@ -0,0 +1,52 @@
/*
* 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 COMMON_THREAD_SAFE_MAP_H_
#define COMMON_THREAD_SAFE_MAP_H_
#include <unordered_map>
#include "absl/synchronization/mutex.h"
namespace cdc_ft {
// A map with keys of type |key_type| and values of type |mapped_type|.
template <class key_type, class mapped_type>
class ThreadSafeMap {
public:
// Returns a value by the |key|. If the |key| is not present,
// returns a default value of the `mapped_type`.
mapped_type Get(const key_type& key) const ABSL_LOCKS_EXCLUDED(mutex_) {
absl::ReaderMutexLock lock(&mutex_);
auto it = kv_.find(key);
return it == kv_.end() ? mapped_type() : it->second;
}
// Sets |value| to |key|.
void Set(const key_type& key, const mapped_type& value)
ABSL_LOCKS_EXCLUDED(mutex_) {
absl::WriterMutexLock lock(&mutex_);
kv_[key] = value;
}
private:
mutable absl::Mutex mutex_;
std::unordered_map<key_type, mapped_type> kv_ ABSL_GUARDED_BY(mutex_);
};
} // namespace cdc_ft
#endif // COMMON_THREAD_SAFE_MAP_H_

View File

@@ -0,0 +1,59 @@
// 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 "common/thread_safe_map.h"
#include "gtest/gtest.h"
namespace cdc_ft {
namespace {
class ThreadSafeMapTest : public ::testing::Test {};
TEST_F(ThreadSafeMapTest, SetGetExising) {
ThreadSafeMap<std::string, std::string> map_str_str;
std::string key_str = "key 1";
std::string val_str = "val 1";
map_str_str.Set(key_str, val_str);
EXPECT_EQ(map_str_str.Get(key_str), val_str);
ThreadSafeMap<int, bool> map_int_bool;
int key_int = 345;
bool val_bool = true;
map_int_bool.Set(key_int, val_bool);
EXPECT_EQ(map_int_bool.Get(key_int), val_bool);
ThreadSafeMap<int, double> map_int_double;
key_int = 124;
double val_double = 987.23445432;
map_int_double.Set(key_int, val_double);
EXPECT_EQ(map_int_double.Get(key_int), val_double);
}
TEST_F(ThreadSafeMapTest, SetGetMissing) {
ThreadSafeMap<std::string, std::string> map_str_str;
map_str_str.Set("key 1", "val 1");
EXPECT_EQ(map_str_str.Get("key 2"), "");
ThreadSafeMap<int, bool> map_int_bool;
map_int_bool.Set(1287, true);
EXPECT_EQ(map_int_bool.Get(-98), false);
ThreadSafeMap<int, double> map_int_double;
map_int_double.Set(-82534, 876.121232);
EXPECT_EQ(map_int_double.Get(12), 0);
}
} // namespace
} // namespace cdc_ft

122
common/threadpool.cc Normal file
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 "common/threadpool.h"
namespace cdc_ft {
Threadpool::Threadpool(size_t num_threads) : shutdown_(false) {
workers_.reserve(num_threads);
for (size_t n = 0; n < num_threads; ++n) {
workers_.emplace_back([this]() { ThreadWorkerMain(); });
}
}
Threadpool::~Threadpool() { Shutdown(); }
void Threadpool::Wait() {
absl::MutexLock lock(&task_queue_mutex_);
auto cond = [this]() ABSL_EXCLUSIVE_LOCKS_REQUIRED(task_queue_mutex_) {
return outstanding_task_count_ == 0;
};
task_queue_mutex_.Await(absl::Condition(&cond));
}
void Threadpool::Shutdown() {
{
// Signal shutdown.
absl::MutexLock lock(&task_queue_mutex_);
if (shutdown_) return;
shutdown_ = true;
}
// Join thread. This makes sure that the last task finishes.
for (auto& worker : workers_) {
if (worker.joinable()) worker.join();
}
}
void Threadpool::QueueTask(std::unique_ptr<Task> task) {
absl::MutexLock lock(&task_queue_mutex_);
++outstanding_task_count_;
task_queue_.push(std::move(task));
}
std::unique_ptr<Task> Threadpool::TryGetCompletedTask() {
absl::MutexLock lock(&completed_tasks_mutex_);
if (completed_tasks_.empty()) {
return std::unique_ptr<Task>();
}
std::unique_ptr<Task> task = std::move(completed_tasks_.front());
completed_tasks_.pop();
return task;
}
std::unique_ptr<Task> Threadpool::GetCompletedTask() {
absl::MutexLock lock(&completed_tasks_mutex_);
auto cond = [this]() ABSL_EXCLUSIVE_LOCKS_REQUIRED(completed_tasks_mutex_) {
return !completed_tasks_.empty();
};
completed_tasks_mutex_.Await(absl::Condition(&cond));
std::unique_ptr<Task> task = std::move(completed_tasks_.front());
completed_tasks_.pop();
return task;
}
void Threadpool::ThreadWorkerMain() {
bool task_finished = false;
for (;;) {
std::unique_ptr<Task> task;
{
absl::MutexLock lock(&task_queue_mutex_);
// Decrease task count here, so we don't have to lock again at the end of
// the loop.
if (task_finished) {
assert(outstanding_task_count_ > 0);
--outstanding_task_count_;
}
// Wait for task to be available (or shutdown).
auto cond = [this]() ABSL_EXCLUSIVE_LOCKS_REQUIRED(task_queue_mutex_) {
return shutdown_ || !task_queue_.empty();
};
task_queue_mutex_.Await(absl::Condition(&cond));
if (shutdown_) break;
// Grab task from queue.
task = std::move(task_queue_.front());
task_queue_.pop();
}
// Run task, but make it cancellable.
task->ThreadRun([this]() ABSL_EXCLUSIVE_LOCKS_REQUIRED(
task_queue_mutex_) -> bool { return shutdown_; });
{
absl::MutexLock lock(&task_queue_mutex_);
if (shutdown_) break;
}
// Push task to completed queue.
absl::MutexLock lock(&completed_tasks_mutex_);
completed_tasks_.push(std::move(task));
task_finished = true;
}
}
} // namespace cdc_ft

103
common/threadpool.h Normal file
View File

@@ -0,0 +1,103 @@
/*
* 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 COMMON_THREADPOOL_H_
#define COMMON_THREADPOOL_H_
#include <atomic>
#include <condition_variable>
#include <functional>
#include <memory>
#include <queue>
#include <thread>
#include <vector>
#include "absl/synchronization/mutex.h"
namespace cdc_ft {
class Task {
public:
using IsCancelledPredicate = std::function<bool()>;
virtual ~Task() = default;
// Method that's doing all the work.
// Called on a background thread.
virtual void ThreadRun(IsCancelledPredicate is_cancelled) = 0;
};
// Manages a pool of worker threads and schedules tasks to run on the threads.
class Threadpool {
public:
// Creates a new thread pool with |num_threads| worker threads.
explicit Threadpool(size_t num_threads);
~Threadpool();
// Waits for all queued tasks to finish.
void Wait() ABSL_LOCKS_EXCLUDED(task_queue_mutex_);
// Stops the worker threads. Cancels currently active tasks.
void Shutdown() ABSL_LOCKS_EXCLUDED(task_queue_mutex_);
// Queues a task for execution in a worker thread.
void QueueTask(std::unique_ptr<Task> task)
ABSL_LOCKS_EXCLUDED(task_queue_mutex_);
// If available, returns the next completed task.
// For a single worker thread (|num_threads| == 1), tasks are completed in
// FIFO order. This is no longer the case for multiple threads
// (|num_threads| > 1). Tasks that got queued later might complete first.
std::unique_ptr<Task> TryGetCompletedTask()
ABSL_LOCKS_EXCLUDED(completed_tasks_mutex_);
// Returns the next completed task, possibly blocking until it is available.
// For a single worker thread (|num_threads| == 1), tasks are completed in
// FIFO order. This is no longer the case for multiple threads
// (|num_threads| > 1). Tasks that got queued later might complete first.
std::unique_ptr<Task> GetCompletedTask()
ABSL_LOCKS_EXCLUDED(completed_tasks_mutex_);
// Returns the total number of worker threads in the pool.
size_t NumThreads() const { return workers_.size(); }
// Returns the number of tasks that are either queued or in progress.
size_t NumQueuedTasks() const ABSL_LOCKS_EXCLUDED(task_queue_mutex_) {
absl::ReaderMutexLock lock(&task_queue_mutex_);
return outstanding_task_count_;
}
private:
// Background thread worker method. Picks tasks and runs them.
void ThreadWorkerMain()
ABSL_LOCKS_EXCLUDED(task_queue_mutex_, completed_tasks_mutex_);
mutable absl::Mutex task_queue_mutex_;
std::queue<std::unique_ptr<Task>> task_queue_
ABSL_GUARDED_BY(task_queue_mutex_);
size_t outstanding_task_count_ ABSL_GUARDED_BY(task_queue_mutex_) = 0;
std::atomic_bool shutdown_ ABSL_GUARDED_BY(task_queue_mutex_);
absl::Mutex completed_tasks_mutex_;
std::queue<std::unique_ptr<Task>> completed_tasks_
ABSL_GUARDED_BY(completed_tasks_mutex_);
std::vector<std::thread> workers_;
};
} // namespace cdc_ft
#endif // COMMON_THREADPOOL_H_

155
common/threadpool_test.cc Normal file
View File

@@ -0,0 +1,155 @@
// 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 "common/threadpool.h"
#include <atomic>
#include <functional>
#include <unordered_set>
#include "common/semaphore.h"
#include "common/util.h"
#include "gtest/gtest.h"
namespace cdc_ft {
namespace {
// Wrapper class to make it possible to pass lambdas as task functions.
class TestTask : public Task {
public:
using TaskFunc = std::function<void(IsCancelledPredicate is_cancelled)>;
explicit TestTask(TaskFunc task_func) : task_func_(task_func) {}
virtual void ThreadRun(IsCancelledPredicate is_cancelled) {
task_func_(is_cancelled);
}
private:
TaskFunc task_func_;
};
class ThreadpoolTest : public ::testing::Test {};
TEST_F(ThreadpoolTest, WaitShutdownWorkWithoutTasks) {
Threadpool pool(3);
pool.Wait();
pool.Shutdown();
}
TEST_F(ThreadpoolTest, SingleThreadedRunsToCompletion) {
std::atomic_bool task_finished(false);
auto task_func = [&task_finished](Task::IsCancelledPredicate) {
task_finished = true;
};
Threadpool pool(1);
std::unique_ptr<Task> task_ptr = std::make_unique<TestTask>(task_func);
Task* task = task_ptr.get();
pool.QueueTask(std::move(task_ptr));
pool.Wait();
EXPECT_TRUE(task_finished);
std::unique_ptr<Task> completed_task = pool.TryGetCompletedTask();
EXPECT_EQ(completed_task.get(), task);
}
TEST_F(ThreadpoolTest, MultiThreadedRunsToCompletion) {
const int num_tasks = 19;
const int num_threads = 7;
std::atomic_int num_completed(0);
Threadpool pool(num_threads);
std::unordered_set<Task*> tasks;
for (int n = 0; n < num_tasks; ++n) {
auto task_func = [&num_completed](Task::IsCancelledPredicate) {
++num_completed;
};
auto task = std::make_unique<TestTask>(task_func);
tasks.insert(task.get());
pool.QueueTask(std::move(task));
}
pool.Wait();
EXPECT_EQ(num_completed, num_tasks);
for (int n = 0; n < num_tasks; ++n) {
std::unique_ptr<Task> completed_task = pool.TryGetCompletedTask();
EXPECT_TRUE(completed_task);
tasks.erase(completed_task.get());
}
EXPECT_FALSE(pool.TryGetCompletedTask());
EXPECT_TRUE(tasks.empty());
}
TEST_F(ThreadpoolTest, TaskIsCancelledOnShutdown) {
Semaphore task_started(0);
std::atomic_bool task_finished(false);
auto task_func = [&task_started,
&task_finished](Task::IsCancelledPredicate is_cancelled) {
task_started.Signal();
while (!is_cancelled()) {
Util::Sleep(0);
}
task_finished = true;
};
Threadpool pool(1);
pool.QueueTask(std::make_unique<TestTask>(task_func));
task_started.Wait();
pool.Shutdown();
EXPECT_TRUE(task_finished);
// The cancelled task should be discarded.
std::unique_ptr<Task> completed_task = pool.TryGetCompletedTask();
EXPECT_FALSE(completed_task.get());
}
TEST_F(ThreadpoolTest, SingleThreadedCompletesInFifoOrder) {
const int num_tasks = 10;
std::vector<int> completed_order;
Threadpool pool(1);
for (int n = 0; n < num_tasks; ++n) {
auto task_func = [n, &completed_order](Task::IsCancelledPredicate) {
// Thread-safe because there's only one worker thread.
completed_order.push_back(n);
};
pool.QueueTask(std::make_unique<TestTask>(task_func));
}
pool.Wait();
std::unique_ptr<Task> completed_task = pool.TryGetCompletedTask();
ASSERT_EQ(completed_order.size(), num_tasks);
for (int n = 0; n < num_tasks; ++n) {
EXPECT_EQ(completed_order[n], n);
}
}
TEST_F(ThreadpoolTest, GetCompletedTask) {
auto task_func = [](Task::IsCancelledPredicate) { Util::Sleep(10); };
Threadpool pool(1);
std::unique_ptr<Task> task_ptr = std::make_unique<TestTask>(task_func);
Task* task = task_ptr.get();
pool.QueueTask(std::move(task_ptr));
// Note: No pool.Wait().
std::unique_ptr<Task> completed_task = pool.GetCompletedTask();
EXPECT_EQ(completed_task.get(), task);
}
} // namespace
} // namespace cdc_ft

59
common/url.cc Normal file
View File

@@ -0,0 +1,59 @@
// 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 "common/url.h"
#include <ctype.h>
#include <regex>
#include "absl/strings/str_format.h"
namespace cdc_ft {
absl::StatusOr<Url> Url::Parse(const std::string& url_string) {
std::string url_copy = url_string;
std::for_each(url_copy.begin(), url_copy.end(),
[](char& c) { c = tolower(c); });
std::regex re("(https?)://([^:^?/]+)(?::([0-9]+))?.*");
std::smatch match;
if (!std::regex_match(url_copy, match, re))
return absl::Status(
absl::StatusCode::kInvalidArgument,
absl::StrFormat("'%s' is not a valid url.", url_string.c_str()));
Url url;
url.protocol = match[1].str();
url.host = match[2].str();
if (match.size() == 4 && match[3].length() > 0)
url.port = match[3].str();
else if (url.protocol.back() == 's')
url.port = "443";
else
url.port = "80";
return url;
}
Url::Url() = default;
Url::Url(std::string protocol, std::string host, std::string port)
: protocol(protocol), host(host), port(port) {}
Url::~Url() = default;
bool operator==(const Url& lhs, const Url& rhs) {
return lhs.host == rhs.host && lhs.port == rhs.port &&
lhs.protocol == rhs.protocol;
}
} // namespace cdc_ft

42
common/url.h Normal file
View File

@@ -0,0 +1,42 @@
/*
* 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 COMMON_URL_H_
#define COMMON_URL_H_
#include "absl/status/statusor.h"
namespace cdc_ft {
struct Url {
Url();
Url(std::string protocol, std::string host, std::string port);
~Url();
std::string protocol;
std::string host;
std::string port;
// Parses |url_string| to Url struct from a given HTTP(S) URL.
// Uses a very simple parser, e.g. does not unescape host (e.g. foo%20bar).
static absl::StatusOr<Url> Parse(const std::string& url_string);
};
bool operator==(const Url& lhs, const Url& rhs);
} // namespace cdc_ft
#endif // COMMON_URL_H_

52
common/url_test.cc Normal file
View File

@@ -0,0 +1,52 @@
// 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 "common/url.h"
#include "absl/status/statusor.h"
#include "common/status_test_macros.h"
#include "gtest/gtest.h"
namespace cdc_ft {
namespace {
class UrlTest : public ::testing::Test {
protected:
void AssertEqual(absl::StatusOr<Url> actual, Url expected) {
EXPECT_OK(actual);
EXPECT_EQ(actual.value(), expected);
}
};
TEST_F(UrlTest, ParseUrl) {
AssertEqual(Url::Parse("https://hOSt.t.gF.d"),
Url("https", "host.t.gf.d", "443"));
AssertEqual(Url::Parse("http://h"), Url("http", "h", "80"));
AssertEqual(Url::Parse("https://h:8080/path"), Url("https", "h", "8080"));
AssertEqual(Url::Parse("https://h?t=45"), Url("https", "h", "443"));
AssertEqual(Url::Parse("https://h:765?t=45"), Url("https", "h", "765"));
AssertEqual(
Url::Parse(
"https://my-domain.ho.HO.ho:80/path/to/smth?arg1=123&dfd=[g,6,y]"),
Url("https", "my-domain.ho.ho.ho", "80"));
EXPECT_NOT_OK(Url::Parse("httpss://domain"));
EXPECT_NOT_OK(Url::Parse("HOST.com:443/path"));
EXPECT_NOT_OK(Url::Parse("https:/domain"));
EXPECT_NOT_OK(Url::Parse("http//domain"));
EXPECT_NOT_OK(Url::Parse("htps://dmn.ua"));
EXPECT_NOT_OK(Url::Parse("non-url"));
}
} // namespace
} // namespace cdc_ft

298
common/util.cc Normal file
View File

@@ -0,0 +1,298 @@
// 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 "common/util.h"
#include <errno.h>
#include <cassert>
#include "absl/random/random.h"
#include "absl/strings/str_format.h"
#if PLATFORM_LINUX
#include <sys/ioctl.h> // struct winsize, TIOCGWINSZ
#include <unistd.h> // usleep, STDOUT_FILENO, isatty
#elif PLATFORM_WINDOWS
#include <io.h> // _isatty
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#endif
namespace cdc_ft {
namespace {
constexpr char kGuidFormat[] = "%08lX-%04hX-%04hX-%04hX-%04hX%08lX";
}
#if PLATFORM_WINDOWS
// static
std::string Util::WideToUtf8Str(const std::wstring& wchar_str) {
int wchar_size = static_cast<int>(wchar_str.size());
int utf8_size = WideCharToMultiByte(CP_UTF8, 0, wchar_str.c_str(), wchar_size,
nullptr, 0, nullptr, nullptr);
assert(utf8_size != 0 || wchar_size == 0);
std::string utf8_str(utf8_size, 0);
WideCharToMultiByte(
CP_UTF8, 0, wchar_str.c_str(), static_cast<int>(wchar_str.size()),
const_cast<LPSTR>(utf8_str.data()), utf8_size, nullptr, 0);
return utf8_str;
}
#endif // #if PLATFORM_WINDOWS
#if PLATFORM_WINDOWS
// static
std::wstring Util::Utf8ToWideStr(const std::string& utf8_str) {
int utf8_size = static_cast<int>(utf8_str.size());
int wchar_size =
MultiByteToWideChar(CP_UTF8, 0, utf8_str.c_str(), utf8_size, nullptr, 0);
assert(wchar_size != 0 || utf8_size == 0);
std::wstring wchar_str(wchar_size, 0);
MultiByteToWideChar(CP_UTF8, 0, utf8_str.c_str(), utf8_size,
const_cast<LPWSTR>(wchar_str.data()), wchar_size);
return wchar_str;
}
#endif // #if PLATFORM_WINDOWS
#if PLATFORM_WINDOWS
// static
std::string Util::GetWin32Error(uint32_t error) {
LPVOID lpMsgBuf;
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
reinterpret_cast<LPTSTR>(&lpMsgBuf), 0, NULL);
std::string message = WideToUtf8Str((LPCTSTR)lpMsgBuf);
LocalFree(lpMsgBuf);
while (!message.empty() &&
(message.back() == '\r' || message.back() == '\n')) {
message.pop_back();
}
return message;
}
#endif // #if PLATFORM_WINDOWS
#if PLATFORM_WINDOWS
// static
std::string Util::GetLastWin32Error() { return GetWin32Error(GetLastError()); }
#endif // #if PLATFORM_WINDOWS
// static
std::string Util::GetStrError(int err) {
#if PLATFORM_WINDOWS
char msg[80] = {0};
strerror_s(msg, sizeof(msg) - 1, err);
return msg;
#elif PLATFORM_LINUX
return std::strerror(err);
#endif
}
// static
std::string Util::GetLastStrError() { return GetStrError(errno); }
// static
int64_t Util::GetPid() {
#if PLATFORM_WINDOWS
return ::GetCurrentProcessId();
#elif PLATFORM_LINUX
return getpid();
#endif
}
// static
int Util::GetConsoleWidth() {
static constexpr int kDefaultConsoleWidth = 80;
#if PLATFORM_WINDOWS
CONSOLE_SCREEN_BUFFER_INFO info;
if (!GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info)) {
return kDefaultConsoleWidth;
}
return info.srWindow.Right - info.srWindow.Left;
#elif PLATFORM_LINUX
struct winsize size;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &size) != 0) {
return kDefaultConsoleWidth;
}
return size.ws_col;
#endif
}
// static
bool Util::IsTTY() {
#if PLATFORM_WINDOWS
return _isatty(_fileno(stdout));
#elif PLATFORM_LINUX
return isatty(STDOUT_FILENO);
#endif
}
// static
bool Util::IsExecutable(const void* data, size_t size) {
static constexpr uint8_t kElfMagic[] = {0x7fu, 0x45u, 0x4cu, 0x46u};
static constexpr uint8_t kSheBangMagic[] = {'#', '!', '/'};
static constexpr uint8_t kMzMagic[] = {0x4du, 0x5au};
if (size < std::max(std::max(sizeof kElfMagic, sizeof kSheBangMagic),
sizeof kMzMagic)) {
return false;
}
if (memcmp(data, kElfMagic, sizeof(kElfMagic)) == 0) return true;
if (memcmp(data, kSheBangMagic, sizeof(kSheBangMagic)) == 0) return true;
if (memcmp(data, kMzMagic, sizeof(kMzMagic)) == 0) return true;
return false;
}
// static
void Util::WaitForDebugger() {
#if PLATFORM_LINUX
static int s = 0;
while (s == 0) {
Sleep(1);
}
#elif PLATFORM_WINDOWS
__debugbreak();
#endif
}
// static
void Util::Sleep(uint32_t duration_ms) {
#if PLATFORM_LINUX
usleep(duration_ms * 1000);
#elif PLATFORM_WINDOWS
::Sleep(duration_ms);
#endif
}
// static
int Util::Utf8CodePointLen(const char* str) {
// UTF-8 code points are encoded as follows:
// 1-byte: 0xxxxxxx
// 2-byte: 110xxxxx 10xxxxxx
// 3-byte: 1110xxxx 10xxxxxx 10xxxxxx
// 4-byte: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
#define Is10(index) ((ustr[index] & 0xC0) == 0x80)
// Note that this is guaranteed to never access memory beyond \0.
const uint8_t* ustr = reinterpret_cast<const uint8_t*>(str);
if (ustr[0] == 0) return 0;
if ((ustr[0] & 0x80) == 0x00) return 1;
if ((ustr[0] & 0xE0) == 0xC0) return Is10(1) ? 2 : 0;
if ((ustr[0] & 0xF0) == 0xE0) return Is10(1) && Is10(2) ? 3 : 0;
if ((ustr[0] & 0xF8) == 0xF0) return Is10(1) && Is10(2) && Is10(3) ? 4 : 0;
return 0;
#undef Is10
}
std::string Util::GenerateUniqueId() {
absl::BitGen gen;
return absl::StrFormat(
kGuidFormat, absl::Uniform<uint32_t>(gen), absl::Uniform<uint16_t>(gen),
absl::Uniform<uint16_t>(gen), absl::Uniform<uint16_t>(gen),
absl::Uniform<uint16_t>(gen), absl::Uniform<uint32_t>(gen));
}
std::vector<absl::string_view> SplitString(absl::string_view s,
const char delim, bool keep_empty) {
if (s.empty()) return std::vector<absl::string_view>();
// Count no. of parts so that we can pre-allocate the target vector.
int count = 1;
size_t pos = 0;
size_t last_pos = std::string::npos;
for (pos = s.find(delim, pos); pos != std::string::npos;
pos = s.find(delim, pos + 1)) {
if (keep_empty || pos - 1 != last_pos) ++count;
last_pos = pos;
}
// Collect all parts
std::vector<absl::string_view> parts;
parts.reserve(count);
size_t first = 0, last = 0;
for (last = s.find(delim, first); last != std::string::npos;
last = s.find(delim, first)) {
if (keep_empty || first < last) {
parts.push_back(absl::string_view(s.data() + first, last - first));
}
first = last + 1;
}
// Append the last part, if needed
if (keep_empty || first < s.size()) {
parts.push_back(absl::string_view(s.data() + first, s.size() - first));
}
return parts;
}
std::vector<absl::string_view> SplitString(const std::string& s,
const char delim, bool keep_empty) {
absl::string_view sv(s.c_str(), s.size());
return SplitString(sv, delim, keep_empty);
}
std::string HumanBytes(double size, int precision) {
const double threshold = 2048;
if (size < 1024)
return absl::StrFormat("%d bytes", static_cast<size_t>(size));
size /= 1024.0;
std::string units = "KB";
if (size > threshold) {
size /= 1024.0;
units = "MB";
}
if (size > threshold) {
size /= 1024.0;
units = "GB";
}
if (size > threshold) {
size /= 1024.0;
units = "TB";
}
if (size > threshold) {
size /= 1024.0;
units = "PB";
}
return absl::StrFormat("%.*f %s", precision, size, units);
}
std::string HumanDuration(const absl::Duration& d) {
auto sec = absl::ToInt64Seconds(d);
return absl::StrFormat("%02d:%02d", sec / 60, std::abs(sec) % 60);
}
std::string JoinStrings(const std::vector<absl::string_view>& parts,
size_t first, size_t last, const char delim) {
std::string ret;
for (size_t i = first; i < last && i < parts.size(); ++i) {
if (i != first) ret.push_back(delim);
ret.append(parts[i]);
}
return ret;
}
std::string JoinStrings(const std::vector<absl::string_view>& parts,
const char delim) {
if (parts.empty()) return std::string();
return JoinStrings(parts, 0, parts.size(), delim);
}
} // namespace cdc_ft

137
common/util.h Normal file
View File

@@ -0,0 +1,137 @@
/*
* 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 COMMON_UTIL_H_
#define COMMON_UTIL_H_
#include <string>
#include <vector>
#include "absl/strings/string_view.h"
#include "absl/time/time.h"
#include "common/platform.h"
namespace cdc_ft {
// Assorted utilities
class Util {
public:
#if PLATFORM_WINDOWS
// Converts a wide character string to a UTF8 string.
// Illegal sequences are replaced by U+FFFD.
static std::string WideToUtf8Str(const std::wstring& wchar_str);
// Converts a UTF8 string to a wide character string.
// Illegal sequences are replaced by U+FFFD.
static std::wstring Utf8ToWideStr(const std::string& utf8_str);
// Returns the string that corresponds to the Windows error id |error|.
static std::string GetWin32Error(uint32_t error);
// Returns the string that corresponds to the Windows error id GetLastError().
static std::string GetLastWin32Error();
#endif
// Returns the C runtime error for |err|.
static std::string GetStrError(int err);
// Returns the last C runtime error.
static std::string GetLastStrError();
// Returns the id of the current process.
static int64_t GetPid();
// Returns the width or kDefaultConsoleWidth if not running in console mode.
static int GetConsoleWidth();
// Returns true if stdout is associated with a terminal (alias TTY).
static bool IsTTY();
// Determines whether the given |data| matches PE/elf/shebang magic headers.
// The data should be the beginning of a file and at least 4 bytes long.
// The detection might yield false positives, but no false negatives.
static bool IsExecutable(const void* data, size_t size);
// On Windows, waits for a debugger to be attached and starts debugging.
// On Linux, this just runs an infinite loop. To get into the debugger, set
// breakpoint in the loop and move the instruction pointer (yellow arrow in
// Visual Studio) out of the loop.
static void WaitForDebugger();
// Sleeps for |duration_ms| milliseconds.
static void Sleep(uint32_t duration_ms);
// Returns the number of bytes of the first UTF8 code point in str.
// Returns 0 if the code point is not valid.
static int Utf8CodePointLen(const char* str);
// Generates a unique identifier. The identifier is a sequence of base-16
// digits in the following format: '01234567-89AB-CDEF-0123-456789AB'.
static std::string GenerateUniqueId();
};
// Splits the given string |s| at the given delimiter |delim| and returns the
// parts as a vector of string_views. Since a string_view does not copy the
// data, the caller is responsible for keeping the string memory allocated as
// long as the referenced parts need to be accessed.
//
// If |keep_empty| is false, empty parts will not be included in the result,
// otherwise empty string_views might be inserted.
//
// Returns an empty list if the given string is empty. If the delimiter is not
// found in |s|, a list with a single element containing |s| is returned.
std::vector<absl::string_view> SplitString(absl::string_view s,
const char delim,
bool keep_empty = true);
std::vector<absl::string_view> SplitString(const std::string& s,
const char delim,
bool keep_empty = true);
// Combines the given string |parts| to one string, separated by |delim|. Does
// not check if the previous part already ended with |delim|.
std::string JoinStrings(const std::vector<absl::string_view>& parts,
const char delim);
// Combines the given string |parts| in the range [first, last) to one string,
// all separated by |delim|. Does not check if the previous part already ended
// with |delim|. Ignores indices that are out of bounds.
std::string JoinStrings(const std::vector<absl::string_view>& parts,
size_t first, size_t last, const char delim);
// Prints a human-readable representation of the given |size| and decimal
// |precision|, such as "4 KB" or "2.34 MB".
std::string HumanBytes(double size, int precision = 0);
// Prints a human-readable representation of a duration as minutes and seconds
// in the format "mm:ss" (with leading zero for > 10 minutes).
std::string HumanDuration(const absl::Duration& d);
// FinalSetter sets a given |receiver| variable to a given |value| once it gets
// out of scope.
template <typename T>
class FinalSetter {
public:
FinalSetter(T* receiver, T value) : receiver_(receiver), value_(value) {}
~FinalSetter() { *receiver_ = std::move(value_); }
private:
T* receiver_;
T value_;
};
} // namespace cdc_ft
#endif // COMMON_UTIL_H_

288
common/util_test.cc Normal file
View File

@@ -0,0 +1,288 @@
// 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 "common/util.h"
#include <limits>
#include "gtest/gtest.h"
namespace cdc_ft {
namespace {
using StringList = std::vector<absl::string_view>;
#if PLATFORM_WINDOWS
constexpr wchar_t kWcharString[] = L"Hey Google, where's the next ⛽?";
constexpr char kUtf8String[] = u8"Hey Google, where's the next ⛽?";
// Invalid character (\uFFFD) + terminator in oth cases.
constexpr wchar_t kInvalidWcharString[] = {65533, 0};
constexpr char kInvalidUtf8String[] = {-17, -65, -67, 0};
#endif
#if PLATFORM_WINDOWS
TEST(UtilTest, WideToUtf8Str_Valid) {
std::string utf8_string = Util::WideToUtf8Str(kWcharString);
EXPECT_EQ(utf8_string, kUtf8String);
}
#endif
#if PLATFORM_WINDOWS
TEST(UtilTest, WideToUtf8Str_Empty) {
std::string utf8_string = Util::WideToUtf8Str(L"");
EXPECT_TRUE(utf8_string.empty());
}
#endif
#if PLATFORM_WINDOWS
TEST(UtilTest, WideToUtf8Str_Invalid) {
std::wstring wchar_string = Util::Utf8ToWideStr(kInvalidUtf8String);
EXPECT_EQ(wchar_string, kInvalidWcharString);
}
#endif
#if PLATFORM_WINDOWS
TEST(UtilTest, Utf8ToWideStr_Valid) {
std::wstring wchar_string = Util::Utf8ToWideStr(kUtf8String);
EXPECT_EQ(wchar_string, kWcharString);
}
#endif
#if PLATFORM_WINDOWS
TEST(UtilTest, Utf8ToWideStr_Empty) {
std::wstring wchar_string = Util::Utf8ToWideStr("");
EXPECT_TRUE(wchar_string.empty());
}
#endif
#if PLATFORM_WINDOWS
TEST(UtilTest, Utf8ToWideStr_Invalid) {
std::string utf8_string = Util::WideToUtf8Str(kInvalidWcharString);
EXPECT_EQ(utf8_string, kInvalidUtf8String);
}
#endif
#if PLATFORM_WINDOWS
TEST(UtilTest, GetErrorString) {
EXPECT_EQ(Util::GetWin32Error(0), "The operation completed successfully.");
EXPECT_EQ(Util::GetWin32Error(1), "Incorrect function.");
}
#endif
#if PLATFORM_WINDOWS
TEST(UtilTest, GetLastErrorString) {
// Just get some coverage. We don't know the last error.
EXPECT_NE(Util::GetLastWin32Error(), "");
}
#endif
TEST(UtilTest, GetStrError) {
#if PLATFORM_WINDOWS
EXPECT_EQ(Util::GetStrError(0), "No error");
#else
EXPECT_EQ(Util::GetStrError(0), "Success");
#endif
EXPECT_EQ(Util::GetStrError(1), "Operation not permitted");
}
TEST(UtilTest, GetLastStrError) {
// Just get some coverage. We don't know the last error.
EXPECT_NE(Util::GetLastStrError(), "");
}
TEST(UtilTest, GetPid) {
// Just get some coverage. Processes are guaranteed not to return int64 min.
// On Linux, it is an int32, on Windows an uint32.
EXPECT_NE(Util::GetPid(), std::numeric_limits<int64_t>::min());
}
TEST(UtilTest, GetConsoleWidth) {
// Could be run from a console window, we don't know.
EXPECT_GT(Util::GetConsoleWidth(), 0);
}
TEST(UtilTest, IsTTY) {
// Just exercise code, we don't know how the test is run.
Util::IsTTY();
}
TEST(UtilTest, IsExecutable) {
static constexpr uint8_t kWindowsExe[] = {0x4d, 0x5a, 0x90, 0x00, 0x03, 0x00,
0x00, 0x00, 0x04, 0x00, 0x00, 0x00,
0xff, 0xff, 0x00, 0x00};
static constexpr uint8_t kLinuxElf[] = {0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00};
static constexpr uint8_t kBashScript[] = {0x23, 0x21, 0x2f, 0x62, 0x69, 0x6e,
0x2f, 0x73, 0x68, 0x0d, 0x0a, 0x0d,
0x0a, 0x65, 0x63, 0x68};
static constexpr uint8_t kSecretFile[] = {0x44, 0x45, 0x46, 0x47, 0x41, 0x41,
0x48, 0x48, 0x48, 0x48, 0x41, 0x48,
0x48, 0x48, 0x48, 0x41};
EXPECT_TRUE(Util::IsExecutable(kWindowsExe, sizeof(kWindowsExe)));
EXPECT_TRUE(Util::IsExecutable(kLinuxElf, sizeof(kLinuxElf)));
EXPECT_TRUE(Util::IsExecutable(kBashScript, sizeof(kBashScript)));
EXPECT_FALSE(Util::IsExecutable(kSecretFile, sizeof(kSecretFile)));
}
TEST(UtilTest, Sleep) {
// Just get some coverage.
Util::Sleep(0);
}
TEST(UtilTest, Utf8CodePointLen) {
EXPECT_EQ(Util::Utf8CodePointLen(u8""), 0);
EXPECT_EQ(Util::Utf8CodePointLen(u8"a"), 1);
EXPECT_EQ(Util::Utf8CodePointLen(u8"ab"), 1);
EXPECT_EQ(Util::Utf8CodePointLen(u8"\u0200"), 2);
EXPECT_EQ(Util::Utf8CodePointLen(u8"\u5000"), 3);
EXPECT_EQ(Util::Utf8CodePointLen(u8"\U00010000"), 4);
EXPECT_EQ(Util::Utf8CodePointLen(u8"\u0200 only the first char counts"), 2);
// Corrupt some UTF8 character.
char broken[] = u8"\U00010000";
broken[strlen(broken) - 1] = 'd';
EXPECT_EQ(Util::Utf8CodePointLen(broken), 0);
}
TEST(UtilTest, SplitStringSkipEmpty) {
EXPECT_EQ(SplitString(std::string(""), '/', false), StringList());
EXPECT_EQ(SplitString(std::string("a"), '/', false), StringList({"a"}));
EXPECT_EQ(SplitString(std::string("a/b"), '/', false),
StringList({"a", "b"}));
EXPECT_EQ(SplitString(std::string("a//b"), '/', false),
StringList({"a", "b"}));
EXPECT_EQ(SplitString(std::string("/a/b"), '/', false),
StringList({"a", "b"}));
EXPECT_EQ(SplitString(std::string("a/b/"), '/', false),
StringList({"a", "b"}));
EXPECT_EQ(SplitString(std::string("/a/b/"), '/', false),
StringList({"a", "b"}));
EXPECT_EQ(SplitString(std::string("//a//b//"), '/', false),
StringList({"a", "b"}));
EXPECT_EQ(SplitString(std::string("aa/bb/cc"), '/', false),
StringList({"aa", "bb", "cc"}));
}
TEST(UtilTest, SplitStringKeepEmpty) {
EXPECT_EQ(SplitString(std::string(""), ',', true), StringList());
EXPECT_EQ(SplitString(std::string("a"), ',', true), StringList({"a"}));
EXPECT_EQ(SplitString(std::string("a,b"), ',', true), StringList({"a", "b"}));
EXPECT_EQ(SplitString(std::string("a,b,"), ',', true),
StringList({"a", "b", ""}));
EXPECT_EQ(SplitString(std::string("a,,c"), ',', true),
StringList({"a", "", "c"}));
EXPECT_EQ(SplitString(std::string(",b,c"), ',', true),
StringList({"", "b", "c"}));
EXPECT_EQ(SplitString(std::string(",,"), ',', true),
StringList({"", "", ""}));
EXPECT_EQ(SplitString(std::string("aa,bb,"), ',', true),
StringList({"aa", "bb", ""}));
EXPECT_EQ(SplitString(std::string("aa,,cc"), ',', true),
StringList({"aa", "", "cc"}));
EXPECT_EQ(SplitString(std::string(",bb,cc"), ',', true),
StringList({"", "bb", "cc"}));
}
TEST(UtilTest, JoinStrings) {
EXPECT_EQ(JoinStrings(StringList(), ','), std::string());
EXPECT_EQ(JoinStrings(StringList({"a"}), ','), "a");
EXPECT_EQ(JoinStrings(StringList({"a", "b"}), ','), "a,b");
EXPECT_EQ(JoinStrings(StringList({"a", "b", "c"}), ','), "a,b,c");
EXPECT_EQ(JoinStrings(StringList({"a", "b", "c"}), ' '), "a b c");
EXPECT_EQ(JoinStrings(StringList({",", ","}), ','), ",,,");
EXPECT_EQ(JoinStrings(StringList({"", ""}), ','), ",");
}
TEST(UtilTest, JoinStringsPartial) {
EXPECT_EQ(JoinStrings(StringList(), 1, 2, ','), std::string());
EXPECT_EQ(JoinStrings(StringList(), 2, 1, ','), std::string());
EXPECT_EQ(JoinStrings(StringList({"a"}), 0, 0, ','), "");
EXPECT_EQ(JoinStrings(StringList({"a"}), 0, 1, ','), "a");
EXPECT_EQ(JoinStrings(StringList({"a"}), 0, 2, ','), "a");
EXPECT_EQ(JoinStrings(StringList({"a"}), 1, 0, ','), "");
EXPECT_EQ(JoinStrings(StringList({"a", "b", "c"}), 0, 1, ','), "a");
EXPECT_EQ(JoinStrings(StringList({"a", "b", "c"}), 1, 2, ','), "b");
EXPECT_EQ(JoinStrings(StringList({"a", "b", "c"}), 2, 3, ','), "c");
EXPECT_EQ(JoinStrings(StringList({"a", "b", "c"}), 0, 2, ','), "a,b");
EXPECT_EQ(JoinStrings(StringList({"a", "b", "c"}), 0, 3, ','), "a,b,c");
EXPECT_EQ(JoinStrings(StringList({"a", "b", "c"}), 1, 3, ','), "b,c");
EXPECT_EQ(JoinStrings(StringList({"a", "b", "c"}), 0, 10, ','), "a,b,c");
}
TEST(UtilTest, HumanBytes) {
EXPECT_EQ(HumanBytes(42), "42 bytes");
EXPECT_EQ(HumanBytes(42ull << 10), "42 KB");
EXPECT_EQ(HumanBytes(42ull << 20), "42 MB");
EXPECT_EQ(HumanBytes(42ull << 30), "42 GB");
EXPECT_EQ(HumanBytes(42ull << 40), "42 TB");
EXPECT_EQ(HumanBytes(42ull << 50), "42 PB");
EXPECT_EQ(HumanBytes(42.5 * 1024), "42 KB");
EXPECT_EQ(HumanBytes(42.5 * 1024, 0), "42 KB");
EXPECT_EQ(HumanBytes(42.5 * 1024, 1), "42.5 KB");
EXPECT_EQ(HumanBytes(42.5 * 1024, 2), "42.50 KB");
// Special case: values returned in byte and the precision is ignored, as it
// does not make sense for human-readable bytes to be fractional.
EXPECT_EQ(HumanBytes(42.5, 2), "42 bytes");
}
TEST(UtilTest, HumanDuration) {
EXPECT_EQ(HumanDuration(absl::Duration()), "00:00");
EXPECT_EQ(HumanDuration(absl::Seconds(5)), "00:05");
EXPECT_EQ(HumanDuration(absl::Seconds(59)), "00:59");
EXPECT_EQ(HumanDuration(absl::Seconds(359)), "05:59");
EXPECT_EQ(HumanDuration(absl::Seconds(12 * 60 + 34)), "12:34");
EXPECT_EQ(HumanDuration(absl::Minutes(2)), "02:00");
EXPECT_EQ(HumanDuration(absl::Minutes(42)), "42:00");
EXPECT_EQ(HumanDuration(absl::Minutes(123)), "123:00");
}
TEST(UtilTest, FinalSetter) {
int i = 0;
{
FinalSetter<int> fs(&i, 42);
EXPECT_EQ(i, 0);
}
EXPECT_EQ(i, 42);
std::string s = "foo";
{
FinalSetter<std::string> fs(&s, "bar");
EXPECT_EQ(s, "foo");
}
EXPECT_EQ(s, "bar");
}
TEST(UtilTest, GenerateUniqueId) {
std::string id = Util::GenerateUniqueId();
EXPECT_EQ(id.size(), 36);
for (int i = 0; i < 36; ++i) {
// Check that dashes are on correct positions.
if (i == 8 || i == 13 || i == 18 || i == 23) {
EXPECT_EQ(id[i], '-');
} else {
EXPECT_TRUE(id[i] >= '0' && id[i] <= '9' || id[i] >= 'A' && id[i] <= 'F');
}
}
EXPECT_NE(Util::GenerateUniqueId(), id);
}
// Can't cover Util::WaitForDebugger(), obviously.
} // namespace
} // namespace cdc_ft