mirror of
https://github.com/nestriness/cdc-file-transfer.git
synced 2026-01-30 14:35:37 +02:00
3
cdc_stream/.gitignore
vendored
Normal file
3
cdc_stream/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
x64/*
|
||||
*.log
|
||||
*.user
|
||||
250
cdc_stream/BUILD
Normal file
250
cdc_stream/BUILD
Normal file
@@ -0,0 +1,250 @@
|
||||
load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library", "cc_test")
|
||||
|
||||
package(default_visibility = [
|
||||
"//:__subpackages__",
|
||||
])
|
||||
|
||||
cc_binary(
|
||||
name = "cdc_stream",
|
||||
srcs = ["main.cc"],
|
||||
data = [":roots_pem"],
|
||||
deps = [
|
||||
":start_command",
|
||||
":start_service_command",
|
||||
":stop_command",
|
||||
"//common:log",
|
||||
"//common:path",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "base_command",
|
||||
srcs = ["base_command.cc"],
|
||||
hdrs = ["base_command.h"],
|
||||
deps = [
|
||||
"//absl_helper:jedec_size_flag",
|
||||
"@com_github_lyra//:lyra",
|
||||
"@com_google_absl//absl/status",
|
||||
"@com_google_absl//absl/strings:str_format",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "start_service_command",
|
||||
srcs = ["start_service_command.cc"],
|
||||
hdrs = ["start_service_command.h"],
|
||||
deps = [
|
||||
":asset_stream_config",
|
||||
":base_command",
|
||||
":session_management_server",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "start_command",
|
||||
srcs = ["start_command.cc"],
|
||||
hdrs = ["start_command.h"],
|
||||
deps = [
|
||||
":base_command",
|
||||
":local_assets_stream_manager_client",
|
||||
":session_management_server",
|
||||
"//common:path",
|
||||
"//common:status_macros",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "stop_command",
|
||||
srcs = ["stop_command.cc"],
|
||||
hdrs = ["stop_command.h"],
|
||||
deps = [
|
||||
":base_command",
|
||||
":local_assets_stream_manager_client",
|
||||
":session_management_server",
|
||||
"//common:path",
|
||||
"//common:remote_util",
|
||||
"//common:status_macros",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "local_assets_stream_manager_client",
|
||||
srcs = ["local_assets_stream_manager_client.cc"],
|
||||
hdrs = ["local_assets_stream_manager_client.h"],
|
||||
deps = [
|
||||
"//common:grpc_status",
|
||||
"//proto:local_assets_stream_manager_grpc_proto",
|
||||
"@com_google_absl//absl/status",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "asset_stream_server",
|
||||
srcs = [
|
||||
"asset_stream_server.cc",
|
||||
"grpc_asset_stream_server.cc",
|
||||
"testing_asset_stream_server.cc",
|
||||
],
|
||||
hdrs = [
|
||||
"asset_stream_server.h",
|
||||
"grpc_asset_stream_server.h",
|
||||
"testing_asset_stream_server.h",
|
||||
],
|
||||
deps = [
|
||||
"//common:grpc_status",
|
||||
"//common:log",
|
||||
"//common:path",
|
||||
"//common:status",
|
||||
"//common:status_macros",
|
||||
"//common:thread_safe_map",
|
||||
"//data_store",
|
||||
"//manifest:manifest_updater",
|
||||
"//proto:asset_stream_service_grpc_proto",
|
||||
"@com_google_absl//absl/strings:str_format",
|
||||
"@com_google_absl//absl/time",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "asset_stream_config",
|
||||
srcs = ["asset_stream_config.cc"],
|
||||
hdrs = ["asset_stream_config.h"],
|
||||
deps = [
|
||||
":base_command",
|
||||
":multi_session",
|
||||
"//common:log",
|
||||
"//common:path",
|
||||
"//common:status_macros",
|
||||
"//data_store:data_provider",
|
||||
"//data_store:disk_data_store",
|
||||
"@com_github_jsoncpp//:jsoncpp",
|
||||
"@com_github_lyra//:lyra",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "cdc_fuse_manager",
|
||||
srcs = ["cdc_fuse_manager.cc"],
|
||||
hdrs = ["cdc_fuse_manager.h"],
|
||||
deps = [
|
||||
"//cdc_fuse_fs:constants",
|
||||
"//common:gamelet_component",
|
||||
"//common:remote_util",
|
||||
"//common:status_macros",
|
||||
"@com_google_absl//absl/status",
|
||||
"@com_google_absl//absl/strings:str_format",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "session_management_server",
|
||||
srcs = [
|
||||
"background_service_impl.cc",
|
||||
"background_service_impl.h",
|
||||
"local_assets_stream_manager_service_impl.cc",
|
||||
"local_assets_stream_manager_service_impl.h",
|
||||
"session_management_server.cc",
|
||||
"session_manager.cc",
|
||||
"session_manager.h",
|
||||
],
|
||||
hdrs = ["session_management_server.h"],
|
||||
deps = [
|
||||
":multi_session",
|
||||
"//common:grpc_status",
|
||||
"//common:log",
|
||||
"//common:status_macros",
|
||||
"//common:util",
|
||||
"//manifest:manifest_updater",
|
||||
"//metrics",
|
||||
"//proto:background_service_grpc_proto",
|
||||
"//proto:local_assets_stream_manager_grpc_proto",
|
||||
"@com_google_absl//absl/strings",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "multi_session",
|
||||
srcs = [
|
||||
"multi_session.cc",
|
||||
"session.cc",
|
||||
"session.h",
|
||||
],
|
||||
hdrs = [
|
||||
"multi_session.h",
|
||||
"session_config.h",
|
||||
],
|
||||
deps = [
|
||||
":asset_stream_server",
|
||||
":cdc_fuse_manager",
|
||||
":metrics_recorder",
|
||||
"//common:file_watcher",
|
||||
"//common:log",
|
||||
"//common:path",
|
||||
"//common:port_manager",
|
||||
"//common:process",
|
||||
"//common:remote_util",
|
||||
"//common:sdk_util",
|
||||
"//common:status_macros",
|
||||
"//common:stopwatch",
|
||||
"//data_store:disk_data_store",
|
||||
"//manifest:manifest_printer",
|
||||
"//manifest:manifest_updater",
|
||||
"@com_google_absl//absl/status",
|
||||
],
|
||||
)
|
||||
|
||||
cc_test(
|
||||
name = "multi_session_test",
|
||||
srcs = ["multi_session_test.cc"],
|
||||
data = [":all_test_data"],
|
||||
deps = [
|
||||
":multi_session",
|
||||
"//common:test_main",
|
||||
"//manifest:manifest_test_base",
|
||||
"@com_google_googletest//:gtest",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "metrics_recorder",
|
||||
srcs = ["metrics_recorder.cc"],
|
||||
hdrs = ["metrics_recorder.h"],
|
||||
deps = [
|
||||
"//common:log",
|
||||
"//common:util",
|
||||
"//metrics",
|
||||
"//metrics:enums",
|
||||
"//metrics:messages",
|
||||
"@com_google_absl//absl/status",
|
||||
],
|
||||
)
|
||||
|
||||
cc_test(
|
||||
name = "metrics_recorder_test",
|
||||
srcs = ["metrics_recorder_test.cc"],
|
||||
deps = [
|
||||
":metrics_recorder",
|
||||
"//common:status_test_macros",
|
||||
"//common:test_main",
|
||||
"//metrics",
|
||||
"@com_google_googletest//:gtest",
|
||||
],
|
||||
)
|
||||
|
||||
# Copy roots.pem to the output folder, required for authenticated gRPC.
|
||||
genrule(
|
||||
name = "roots_pem",
|
||||
srcs = ["@com_github_grpc_grpc//:root_certificates"],
|
||||
outs = ["roots.pem"],
|
||||
cmd = "cp $(location @com_github_grpc_grpc//:root_certificates) $(location roots.pem)",
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all_test_sources",
|
||||
srcs = glob(["*_test.cc"]),
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all_test_data",
|
||||
srcs = glob(["testdata/**"]),
|
||||
)
|
||||
259
cdc_stream/asset_stream_config.cc
Normal file
259
cdc_stream/asset_stream_config.cc
Normal file
@@ -0,0 +1,259 @@
|
||||
// Copyright 2022 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "cdc_stream/asset_stream_config.h"
|
||||
|
||||
#include <sstream>
|
||||
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "absl/strings/str_join.h"
|
||||
#include "absl_helper/jedec_size_flag.h"
|
||||
#include "cdc_stream/base_command.h"
|
||||
#include "common/buffer.h"
|
||||
#include "common/path.h"
|
||||
#include "common/status_macros.h"
|
||||
#include "data_store/data_provider.h"
|
||||
#include "data_store/disk_data_store.h"
|
||||
#include "json/json.h"
|
||||
#include "lyra/lyra.hpp"
|
||||
|
||||
namespace cdc_ft {
|
||||
namespace {
|
||||
constexpr int kDefaultVerbosity = 2;
|
||||
constexpr uint32_t kDefaultManifestUpdaterThreads = 4;
|
||||
constexpr uint32_t kDefaultFileChangeWaitDurationMs = 500;
|
||||
} // namespace
|
||||
|
||||
AssetStreamConfig::AssetStreamConfig() = default;
|
||||
|
||||
AssetStreamConfig::~AssetStreamConfig() = default;
|
||||
|
||||
void AssetStreamConfig::RegisterCommandLineFlags(lyra::command& cmd,
|
||||
BaseCommand& base_command) {
|
||||
session_cfg_.verbosity = kDefaultVerbosity;
|
||||
cmd.add_argument(lyra::opt(session_cfg_.verbosity, "num")
|
||||
.name("--verbosity")
|
||||
.help("Verbosity of the log output, default: " +
|
||||
std::to_string(kDefaultVerbosity) +
|
||||
". Increase to make logs more verbose."));
|
||||
|
||||
cmd.add_argument(
|
||||
lyra::opt(session_cfg_.stats)
|
||||
.name("--stats")
|
||||
.help("Collect and print detailed streaming statistics"));
|
||||
|
||||
cmd.add_argument(
|
||||
lyra::opt(session_cfg_.quiet)
|
||||
.name("--quiet")
|
||||
.help("Do not print any output except errors and stats"));
|
||||
|
||||
session_cfg_.manifest_updater_threads = kDefaultManifestUpdaterThreads;
|
||||
cmd.add_argument(lyra::opt(session_cfg_.manifest_updater_threads, "count")
|
||||
.name("--manifest-updater-threads")
|
||||
.help("Number of threads used to compute file hashes on "
|
||||
"the workstation, default: " +
|
||||
std::to_string(kDefaultManifestUpdaterThreads)));
|
||||
|
||||
session_cfg_.file_change_wait_duration_ms = kDefaultFileChangeWaitDurationMs;
|
||||
cmd.add_argument(
|
||||
lyra::opt(session_cfg_.file_change_wait_duration_ms, "ms")
|
||||
.name("--file-change-wait-duration-ms")
|
||||
.help("Time in milliseconds to wait until pushing a file change "
|
||||
"to the instance after detecting it, default: " +
|
||||
std::to_string(kDefaultFileChangeWaitDurationMs)));
|
||||
|
||||
cmd.add_argument(lyra::opt(session_cfg_.fuse_debug)
|
||||
.name("--debug")
|
||||
.help("Run FUSE filesystem in debug mode"));
|
||||
|
||||
cmd.add_argument(lyra::opt(session_cfg_.fuse_singlethreaded)
|
||||
.name("--singlethreaded")
|
||||
.optional()
|
||||
.help("Run FUSE filesystem in single-threaded mode"));
|
||||
|
||||
cmd.add_argument(lyra::opt(session_cfg_.fuse_check)
|
||||
.name("--check")
|
||||
.help("Check FUSE consistency and log check results"));
|
||||
|
||||
session_cfg_.fuse_cache_capacity = DiskDataStore::kDefaultCapacity;
|
||||
cmd.add_argument(
|
||||
lyra::opt(base_command.JedecParser("--cache-capacity",
|
||||
&session_cfg_.fuse_cache_capacity),
|
||||
"bytes")
|
||||
.name("--cache-capacity")
|
||||
.help("FUSE cache capacity, default: " +
|
||||
std::to_string(DiskDataStore::kDefaultCapacity) +
|
||||
". Supports common unit suffixes K, M, G."));
|
||||
|
||||
session_cfg_.fuse_cleanup_timeout_sec = DataProvider::kCleanupTimeoutSec;
|
||||
cmd.add_argument(
|
||||
lyra::opt(session_cfg_.fuse_cleanup_timeout_sec, "sec")
|
||||
.name("--cleanup-timeout")
|
||||
.help("Period in seconds at which instance cache cleanups are run, "
|
||||
"default: " +
|
||||
std::to_string(DataProvider::kCleanupTimeoutSec)));
|
||||
|
||||
session_cfg_.fuse_access_idle_timeout_sec = DataProvider::kAccessIdleSec;
|
||||
cmd.add_argument(
|
||||
lyra::opt(session_cfg_.fuse_access_idle_timeout_sec, "sec")
|
||||
.name("--access-idle-timeout")
|
||||
.help("Do not run instance cache cleanups for this long after the "
|
||||
"last file access, default: " +
|
||||
std::to_string(DataProvider::kAccessIdleSec)));
|
||||
|
||||
cmd.add_argument(lyra::opt(log_to_stdout_)
|
||||
.name("--log-to-stdout")
|
||||
.help("Log to stdout instead of to a file"));
|
||||
cmd.add_argument(
|
||||
lyra::opt(dev_src_dir_, "dir")
|
||||
.name("--dev-src-dir")
|
||||
.help("Start a streaming session immediately from the given Windows "
|
||||
"path. Used during development. Must also specify other --dev "
|
||||
"flags."));
|
||||
|
||||
cmd.add_argument(
|
||||
lyra::opt(dev_target_.user_host, "[user@]host")
|
||||
.name("--dev-user-host")
|
||||
.help("Username and host to stream to. See also --dev-src-dir."));
|
||||
|
||||
dev_target_.ssh_port = RemoteUtil::kDefaultSshPort;
|
||||
cmd.add_argument(
|
||||
lyra::opt(dev_target_.ssh_port, "port")
|
||||
.name("--dev-ssh-port")
|
||||
.help("SSH port to use for the connection to the host, default: " +
|
||||
std::to_string(RemoteUtil::kDefaultSshPort) +
|
||||
". See also --dev-src-dir."));
|
||||
|
||||
cmd.add_argument(
|
||||
lyra::opt(dev_target_.ssh_command, "cmd")
|
||||
.name("--dev-ssh-command")
|
||||
.help("Ssh command and extra flags to use for the "
|
||||
"connection to the host. See also --dev-src-dir."));
|
||||
|
||||
cmd.add_argument(
|
||||
lyra::opt(dev_target_.scp_command, "cmd")
|
||||
.name("--dev-scp-command")
|
||||
.help("Scp command and extra flags to use for the "
|
||||
"connection to the host. See also --dev-src-dir."));
|
||||
|
||||
cmd.add_argument(
|
||||
lyra::opt(dev_target_.mount_dir, "dir")
|
||||
.name("--dev-mount-dir")
|
||||
.help("Directory on the host to stream to. See also --dev-src-dir."));
|
||||
}
|
||||
|
||||
absl::Status AssetStreamConfig::LoadFromFile(const std::string& path) {
|
||||
Buffer buffer;
|
||||
RETURN_IF_ERROR(path::ReadFile(path, &buffer));
|
||||
|
||||
Json::Value config;
|
||||
Json::Reader reader;
|
||||
if (!reader.parse(buffer.data(), buffer.data() + buffer.size(), config,
|
||||
false)) {
|
||||
return absl::InvalidArgumentError(
|
||||
absl::StrFormat("Failed to parse config file '%s': %s", path,
|
||||
reader.getFormattedErrorMessages()));
|
||||
}
|
||||
|
||||
#define ASSIGN_VAR(var, flag, type) \
|
||||
do { \
|
||||
if (config.isMember(flag)) { \
|
||||
var = config[flag].as##type(); \
|
||||
flags_read_from_file_.insert(flag); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
ASSIGN_VAR(session_cfg_.verbosity, "verbosity", Int);
|
||||
ASSIGN_VAR(session_cfg_.fuse_debug, "debug", Bool);
|
||||
ASSIGN_VAR(session_cfg_.fuse_singlethreaded, "singlethreaded", Bool);
|
||||
ASSIGN_VAR(session_cfg_.stats, "stats", Bool);
|
||||
ASSIGN_VAR(session_cfg_.quiet, "quiet", Bool);
|
||||
ASSIGN_VAR(session_cfg_.fuse_check, "check", Bool);
|
||||
ASSIGN_VAR(log_to_stdout_, "log-to-stdout", Bool);
|
||||
ASSIGN_VAR(session_cfg_.fuse_cleanup_timeout_sec, "cleanup-timeout", Int);
|
||||
ASSIGN_VAR(session_cfg_.fuse_access_idle_timeout_sec, "access-idle-timeout",
|
||||
Int);
|
||||
ASSIGN_VAR(session_cfg_.manifest_updater_threads, "manifest-updater-threads",
|
||||
Int);
|
||||
ASSIGN_VAR(session_cfg_.file_change_wait_duration_ms,
|
||||
"file-change-wait-duration-ms", Int);
|
||||
|
||||
// cache_capacity requires Jedec size conversion.
|
||||
constexpr char kCacheCapacity[] = "cache-capacity";
|
||||
if (config.isMember(kCacheCapacity)) {
|
||||
JedecSize cache_capacity;
|
||||
std::string error;
|
||||
if (AbslParseFlag(config[kCacheCapacity].asString(), &cache_capacity,
|
||||
&error)) {
|
||||
session_cfg_.fuse_cache_capacity = cache_capacity.Size();
|
||||
flags_read_from_file_.insert(kCacheCapacity);
|
||||
} else {
|
||||
// Note that |error| can't be logged here since this code runs before
|
||||
// logging is initialized.
|
||||
flag_read_errors_[kCacheCapacity] = error;
|
||||
}
|
||||
}
|
||||
|
||||
#undef ASSIGN_VAR
|
||||
|
||||
return absl::OkStatus();
|
||||
} // namespace cdc_ft
|
||||
|
||||
std::string AssetStreamConfig::ToString() {
|
||||
std::ostringstream ss;
|
||||
ss << "verbosity = " << session_cfg_.verbosity
|
||||
<< std::endl;
|
||||
ss << "debug = " << session_cfg_.fuse_debug
|
||||
<< std::endl;
|
||||
ss << "singlethreaded = " << session_cfg_.fuse_singlethreaded
|
||||
<< std::endl;
|
||||
ss << "stats = " << session_cfg_.stats << std::endl;
|
||||
ss << "quiet = " << session_cfg_.quiet << std::endl;
|
||||
ss << "check = " << session_cfg_.fuse_check
|
||||
<< std::endl;
|
||||
ss << "log-to-stdout = " << log_to_stdout_ << std::endl;
|
||||
ss << "cache-capacity = " << session_cfg_.fuse_cache_capacity
|
||||
<< std::endl;
|
||||
ss << "cleanup-timeout = "
|
||||
<< session_cfg_.fuse_cleanup_timeout_sec << std::endl;
|
||||
ss << "access-idle-timeout = "
|
||||
<< session_cfg_.fuse_access_idle_timeout_sec << std::endl;
|
||||
ss << "manifest-updater-threads = "
|
||||
<< session_cfg_.manifest_updater_threads << std::endl;
|
||||
ss << "file-change-wait-duration-ms = "
|
||||
<< session_cfg_.file_change_wait_duration_ms << std::endl;
|
||||
ss << "dev-src-dir = " << dev_src_dir_ << std::endl;
|
||||
ss << "dev-user-host = " << dev_target_.user_host << std::endl;
|
||||
ss << "dev-ssh-port = " << dev_target_.ssh_port << std::endl;
|
||||
ss << "dev-ssh-command = " << dev_target_.ssh_command
|
||||
<< std::endl;
|
||||
ss << "dev-scp-command = " << dev_target_.scp_command
|
||||
<< std::endl;
|
||||
ss << "dev-mount-dir = " << dev_target_.mount_dir << std::endl;
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string AssetStreamConfig::GetFlagsReadFromFile() {
|
||||
return absl::StrJoin(flags_read_from_file_, ", ");
|
||||
}
|
||||
|
||||
std::string AssetStreamConfig::GetFlagReadErrors() {
|
||||
std::string error_str;
|
||||
for (const auto& [flag, error] : flag_read_errors_)
|
||||
error_str += absl::StrFormat("%sFailed to read '%s': %s",
|
||||
error_str.empty() ? "" : "\n", flag, error);
|
||||
return error_str;
|
||||
}
|
||||
|
||||
} // namespace cdc_ft
|
||||
111
cdc_stream/asset_stream_config.h
Normal file
111
cdc_stream/asset_stream_config.h
Normal 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 CDC_STREAM_ASSET_STREAM_CONFIG_H_
|
||||
#define CDC_STREAM_SET_STREAM_CONFIG_H_
|
||||
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "cdc_stream/session_config.h"
|
||||
#include "session.h"
|
||||
|
||||
namespace lyra {
|
||||
class command;
|
||||
}
|
||||
|
||||
namespace cdc_ft {
|
||||
|
||||
class BaseCommand;
|
||||
|
||||
// Class containing all configuration settings for asset streaming.
|
||||
// Reads flags from the command line and optionally applies overrides from
|
||||
// a json file.
|
||||
class AssetStreamConfig {
|
||||
public:
|
||||
// Constructs the configuration by applying command line flags.
|
||||
AssetStreamConfig();
|
||||
~AssetStreamConfig();
|
||||
|
||||
// Registers arguments with Lyra.
|
||||
void RegisterCommandLineFlags(lyra::command& cmd, BaseCommand& base_command);
|
||||
|
||||
// Loads a configuration from the JSON file at |path| and overrides any config
|
||||
// values that are set in this file. Sample json file:
|
||||
// {
|
||||
// "verbosity":3,
|
||||
// "debug":0,
|
||||
// "singlethreaded":0,
|
||||
// "stats":0,
|
||||
// "quiet":0,
|
||||
// "check":0,
|
||||
// "log_to_stdout":0,
|
||||
// "cache_capacity":"150G",
|
||||
// "cleanup_timeout":300,
|
||||
// "access_idle_timeout":5,
|
||||
// "manifest_updater_threads":4,
|
||||
// "file_change_wait_duration_ms":500
|
||||
// }
|
||||
// Returns NotFoundError if the file does not exist.
|
||||
// Returns InvalidArgumentError if the file is not valid JSON.
|
||||
absl::Status LoadFromFile(const std::string& path);
|
||||
|
||||
// Returns a string with all config values, suitable for logging.
|
||||
std::string ToString();
|
||||
|
||||
// Gets a comma-separated list of flags that were read from the JSON file.
|
||||
// These flags override command line flags.
|
||||
std::string GetFlagsReadFromFile();
|
||||
|
||||
// Gets a newline-separated list of errors for each flag that could not be
|
||||
// read from the JSON file.
|
||||
std::string GetFlagReadErrors();
|
||||
|
||||
// Session configuration.
|
||||
const SessionConfig& session_cfg() const { return session_cfg_; }
|
||||
|
||||
// Workstation directory to be streamed. Used for development purposes only
|
||||
// to start a session right away when the service starts up. See dev CLI args.
|
||||
const std::string& dev_src_dir() const { return dev_src_dir_; }
|
||||
|
||||
// Session target. Used for development purposes only to start a session right
|
||||
// away when the service starts up. See dev CLI args.
|
||||
const SessionTarget& dev_target() const { return dev_target_; }
|
||||
|
||||
// Whether to log to a file or to stdout.
|
||||
bool log_to_stdout() const { return log_to_stdout_; }
|
||||
|
||||
private:
|
||||
SessionConfig session_cfg_;
|
||||
bool log_to_stdout_ = false;
|
||||
|
||||
// Configuration used for development. Allows users to specify a session
|
||||
// via the service's command line.
|
||||
std::string dev_src_dir_;
|
||||
SessionTarget dev_target_;
|
||||
|
||||
// Use a set, so the flags are sorted alphabetically.
|
||||
std::set<std::string> flags_read_from_file_;
|
||||
|
||||
// Maps flags to errors occurred while reading this flag.
|
||||
std::map<std::string, std::string> flag_read_errors_;
|
||||
};
|
||||
|
||||
}; // namespace cdc_ft
|
||||
|
||||
#endif // CDC_STREAM_SET_STREAM_CONFIG_H_
|
||||
42
cdc_stream/asset_stream_server.cc
Normal file
42
cdc_stream/asset_stream_server.cc
Normal 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.
|
||||
|
||||
#include "cdc_stream/asset_stream_server.h"
|
||||
|
||||
#include "cdc_stream/grpc_asset_stream_server.h"
|
||||
#include "cdc_stream/testing_asset_stream_server.h"
|
||||
|
||||
namespace cdc_ft {
|
||||
|
||||
AssetStreamServer::AssetStreamServer(std::string src_dir,
|
||||
DataStoreReader* data_store_reader,
|
||||
FileChunkMap* file_chunks) {}
|
||||
|
||||
std::unique_ptr<AssetStreamServer> AssetStreamServer::Create(
|
||||
AssetStreamServerType type, std::string src_dir,
|
||||
DataStoreReader* data_store_reader, FileChunkMap* file_chunks,
|
||||
ContentSentHandler content_sent, PrioritizeAssetsHandler prio_assets) {
|
||||
switch (type) {
|
||||
case AssetStreamServerType::kGrpc:
|
||||
return std::make_unique<GrpcAssetStreamServer>(
|
||||
src_dir, data_store_reader, file_chunks, content_sent, prio_assets);
|
||||
case AssetStreamServerType::kTest:
|
||||
return std::make_unique<TestingAssetStreamServer>(
|
||||
src_dir, data_store_reader, file_chunks);
|
||||
}
|
||||
assert(false);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace cdc_ft
|
||||
97
cdc_stream/asset_stream_server.h
Normal file
97
cdc_stream/asset_stream_server.h
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2022 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef CDC_STREAM_ASSET_STREAM_SERVER_H_
|
||||
#define CDC_STREAM_ASSET_STREAM_SERVER_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/time/time.h"
|
||||
#include "manifest/manifest_proto_defs.h"
|
||||
|
||||
namespace cdc_ft {
|
||||
|
||||
// Handles an event when content is transmitted from the workstation to a
|
||||
// gamelet.
|
||||
// |byte_count| number of bytes transferred during the session so far.
|
||||
// |chunk_count| number of chunks transferred during the session so far.
|
||||
// |instance_id| instance id, which identifies the session.
|
||||
using ContentSentHandler = std::function<void(
|
||||
size_t byte_count, size_t chunk_count, std::string instance_id)>;
|
||||
|
||||
// Handles requests to prioritize the given list of assets while updating the
|
||||
// manifest. |rel_paths| relative Unix paths of assets to prioritize.
|
||||
using PrioritizeAssetsHandler =
|
||||
std::function<void(std::vector<std::string> rel_paths)>;
|
||||
|
||||
class DataStoreReader;
|
||||
class FileChunkMap;
|
||||
|
||||
enum class AssetStreamServerType { kGrpc, kTest };
|
||||
|
||||
class AssetStreamServer {
|
||||
public:
|
||||
// Returns AssetStreamServer of |type|.
|
||||
// |src_dir| is the directory on the workstation to mount.
|
||||
// |data_store_reader| is responsible for loading content by ID.
|
||||
// |file_chunks| is used for mapping data chunk ids to file locations.
|
||||
// |content_sent| handles event when data is transferred from the workstation
|
||||
// to a gamelet.
|
||||
static std::unique_ptr<AssetStreamServer> Create(
|
||||
AssetStreamServerType type, std::string src_dir,
|
||||
DataStoreReader* data_store_reader, FileChunkMap* file_chunks,
|
||||
ContentSentHandler content_sent, PrioritizeAssetsHandler prio_assets);
|
||||
|
||||
AssetStreamServer(const AssetStreamServer& other) = delete;
|
||||
AssetStreamServer& operator=(const AssetStreamServer& other) = delete;
|
||||
virtual ~AssetStreamServer() = default;
|
||||
|
||||
// Starts the asset stream server on the given |port|.
|
||||
// Asserts that the server is not yet running.
|
||||
virtual absl::Status Start(int port) = 0;
|
||||
|
||||
// Sets |manifest_id| to be distributed to gamelets.
|
||||
// Thread-safe.
|
||||
virtual void SetManifestId(const ContentIdProto& manifest_id) = 0;
|
||||
|
||||
// Waits until the FUSE for the given |instance| id has acknowledged the
|
||||
// reception of the currently set manifest id. Returns a DeadlineExceeded
|
||||
// error if the ack is not received within the given |timeout|.
|
||||
// Thread-safe.
|
||||
virtual absl::Status WaitForManifestAck(const std::string& instance,
|
||||
absl::Duration timeout) = 0;
|
||||
|
||||
// Stops internal services and waits for the server to shut down.
|
||||
virtual void Shutdown() = 0;
|
||||
|
||||
// Returns the used manifest id.
|
||||
// Thread-safe.
|
||||
virtual ContentIdProto GetManifestId() const = 0;
|
||||
|
||||
protected:
|
||||
// Creates a new asset streaming server.
|
||||
// |src_dir| is the directory on the workstation to mount.
|
||||
// |data_store_reader| is responsible for loading content by ID.
|
||||
// |file_chunks| is used for mapping data chunk ids to file locations.
|
||||
AssetStreamServer(std::string src_dir, DataStoreReader* data_store_reader,
|
||||
FileChunkMap* file_chunks);
|
||||
};
|
||||
|
||||
} // namespace cdc_ft
|
||||
|
||||
#endif // CDC_STREAM_ASSET_STREAM_SERVER_H_
|
||||
56
cdc_stream/background_service_impl.cc
Normal file
56
cdc_stream/background_service_impl.cc
Normal 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.
|
||||
|
||||
#include "cdc_stream/background_service_impl.h"
|
||||
|
||||
#include "common/grpc_status.h"
|
||||
#include "common/log.h"
|
||||
#include "common/util.h"
|
||||
#include "grpcpp/grpcpp.h"
|
||||
|
||||
namespace cdc_ft {
|
||||
|
||||
BackgroundServiceImpl::BackgroundServiceImpl() {}
|
||||
|
||||
BackgroundServiceImpl::~BackgroundServiceImpl() = default;
|
||||
|
||||
void BackgroundServiceImpl::SetExitCallback(ExitCallback exit_callback) {
|
||||
exit_callback_ = std::move(exit_callback);
|
||||
}
|
||||
|
||||
grpc::Status BackgroundServiceImpl::Exit(grpc::ServerContext* context,
|
||||
const ExitRequest* request,
|
||||
ExitResponse* response) {
|
||||
LOG_INFO("RPC:Exit");
|
||||
if (exit_callback_) {
|
||||
return ToGrpcStatus(exit_callback_());
|
||||
}
|
||||
return grpc::Status::OK;
|
||||
}
|
||||
|
||||
grpc::Status BackgroundServiceImpl::GetPid(grpc::ServerContext* context,
|
||||
const GetPidRequest* request,
|
||||
GetPidResponse* response) {
|
||||
LOG_INFO("RPC:GetPid");
|
||||
response->set_pid(static_cast<int32_t>(Util::GetPid()));
|
||||
return grpc::Status::OK;
|
||||
}
|
||||
|
||||
grpc::Status BackgroundServiceImpl::HealthCheck(grpc::ServerContext* context,
|
||||
const EmptyProto* request,
|
||||
EmptyProto* response) {
|
||||
return grpc::Status::OK;
|
||||
}
|
||||
|
||||
} // namespace cdc_ft
|
||||
63
cdc_stream/background_service_impl.h
Normal file
63
cdc_stream/background_service_impl.h
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2022 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef CDC_STREAM_BACKGROUND_SERVICE_IMPL_H_
|
||||
#define CDC_STREAM_BACKGROUND_SERVICE_IMPL_H_
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "cdc_stream/background_service_impl.h"
|
||||
#include "cdc_stream/session_management_server.h"
|
||||
#include "grpcpp/grpcpp.h"
|
||||
#include "proto/background_service.grpc.pb.h"
|
||||
|
||||
namespace cdc_ft {
|
||||
|
||||
// Implements a service to manage a background process as a server.
|
||||
// This service is owned by SessionManagementServer.
|
||||
class BackgroundServiceImpl final
|
||||
: public backgroundservice::BackgroundService::Service {
|
||||
public:
|
||||
using ExitRequest = backgroundservice::ExitRequest;
|
||||
using ExitResponse = backgroundservice::ExitResponse;
|
||||
using GetPidRequest = backgroundservice::GetPidRequest;
|
||||
using GetPidResponse = backgroundservice::GetPidResponse;
|
||||
using EmptyProto = google::protobuf::Empty;
|
||||
|
||||
BackgroundServiceImpl();
|
||||
~BackgroundServiceImpl();
|
||||
|
||||
// Exit callback gets called from the Exit() RPC.
|
||||
using ExitCallback = std::function<absl::Status()>;
|
||||
void SetExitCallback(ExitCallback exit_callback);
|
||||
|
||||
grpc::Status Exit(grpc::ServerContext* context, const ExitRequest* request,
|
||||
ExitResponse* response) override;
|
||||
|
||||
grpc::Status GetPid(grpc::ServerContext* context,
|
||||
const GetPidRequest* request,
|
||||
GetPidResponse* response) override;
|
||||
|
||||
grpc::Status HealthCheck(grpc::ServerContext* context,
|
||||
const EmptyProto* request,
|
||||
EmptyProto* response) override;
|
||||
|
||||
private:
|
||||
ExitCallback exit_callback_;
|
||||
};
|
||||
|
||||
} // namespace cdc_ft
|
||||
|
||||
#endif // CDC_STREAM_BACKGROUND_SERVICE_IMPL_H_
|
||||
110
cdc_stream/base_command.cc
Normal file
110
cdc_stream/base_command.cc
Normal file
@@ -0,0 +1,110 @@
|
||||
// Copyright 2022 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "cdc_stream/base_command.h"
|
||||
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "absl_helper/jedec_size_flag.h"
|
||||
#include "lyra/lyra.hpp"
|
||||
|
||||
namespace cdc_ft {
|
||||
|
||||
BaseCommand::BaseCommand(std::string name, std::string help, int* exit_code)
|
||||
: name_(name), help_(help), exit_code_(exit_code) {
|
||||
assert(exit_code_);
|
||||
}
|
||||
|
||||
BaseCommand::~BaseCommand() = default;
|
||||
|
||||
void BaseCommand::Register(lyra::cli& cli) {
|
||||
lyra::command cmd(name_,
|
||||
[this](const lyra::group& g) { this->CommandHandler(g); });
|
||||
cmd.help(help_);
|
||||
cmd.add_argument(lyra::help(show_help_));
|
||||
|
||||
RegisterCommandLineFlags(cmd);
|
||||
|
||||
// Detect extra positional args.
|
||||
cmd.add_argument(lyra::arg(PosArgValidator(&extra_positional_arg_), ""));
|
||||
|
||||
// Register command with CLI.
|
||||
cli.add_argument(std::move(cmd));
|
||||
}
|
||||
|
||||
std::function<void(const std::string&)> BaseCommand::JedecParser(
|
||||
const char* flag_name, uint64_t* bytes) {
|
||||
return [flag_name, bytes,
|
||||
error = &jedec_parse_error_](const std::string& value) {
|
||||
JedecSize size;
|
||||
if (AbslParseFlag(value, &size, error)) {
|
||||
*bytes = size.Size();
|
||||
} else {
|
||||
*error = absl::StrFormat("Failed to parse %s=%s: %s", flag_name, value,
|
||||
*error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
std::function<void(const std::string&)> BaseCommand::PosArgValidator(
|
||||
std::string* str) {
|
||||
return [str, invalid_arg = &invalid_arg_](const std::string& value) {
|
||||
if (!value.empty() && value[0] == '-') {
|
||||
*invalid_arg = value;
|
||||
} else {
|
||||
*str = value;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void BaseCommand::CommandHandler(const lyra::group& g) {
|
||||
// Handle -h, --help.
|
||||
if (show_help_) {
|
||||
std::cout << g;
|
||||
*exit_code_ = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle invalid arguments.
|
||||
if (!invalid_arg_.empty()) {
|
||||
std::cerr << "Error: Unknown parameter '" << invalid_arg_
|
||||
<< "'. Try -h for help." << std::endl;
|
||||
*exit_code_ = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!jedec_parse_error_.empty()) {
|
||||
std::cerr << "Error: " << jedec_parse_error_ << std::endl;
|
||||
*exit_code_ = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!extra_positional_arg_.empty()) {
|
||||
std::cerr << "Error: Extraneous positional argument '"
|
||||
<< extra_positional_arg_ << "'. Try -h for help." << std::endl;
|
||||
*exit_code_ = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Run and print error.
|
||||
absl::Status status = Run();
|
||||
if (!status.ok()) {
|
||||
std::cerr << "Error: " << status.message() << std::endl;
|
||||
}
|
||||
|
||||
// Write status code to |exit_code_|.
|
||||
static_assert(static_cast<int>(absl::StatusCode::kOk) == 0, "kOk not 0");
|
||||
*exit_code_ = static_cast<int>(status.code());
|
||||
}
|
||||
|
||||
} // namespace cdc_ft
|
||||
92
cdc_stream/base_command.h
Normal file
92
cdc_stream/base_command.h
Normal 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.
|
||||
*/
|
||||
|
||||
#ifndef CDC_STREAM_BASE_COMMAND_H_
|
||||
#define CDC_STREAM_BASE_COMMAND_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
|
||||
namespace lyra {
|
||||
class cli;
|
||||
class command;
|
||||
class group;
|
||||
} // namespace lyra
|
||||
|
||||
namespace cdc_ft {
|
||||
|
||||
// Base class for commands that wraps Lyra commands to reduce common
|
||||
// boilerplate like help text display, invalid args and return values from
|
||||
// command execution.
|
||||
class BaseCommand {
|
||||
public:
|
||||
// Creates a new command with given |name| and |help| text. After the command
|
||||
// ran, the status code as returned by Run() is written to |exit_code|.
|
||||
BaseCommand(std::string name, std::string help, int* exit_code);
|
||||
~BaseCommand();
|
||||
|
||||
// Registers the command with Lyra. Must be called before parsing args.
|
||||
void Register(lyra::cli& cli);
|
||||
|
||||
// Jedec parser for Lyra options. Usage:
|
||||
// lyra::opt(JedecParser("size-flag", &size_bytes), "bytes"))
|
||||
// Automatically reports a parse failure on error.
|
||||
std::function<void(const std::string&)> JedecParser(const char* flag_name,
|
||||
uint64_t* bytes);
|
||||
|
||||
// Validator that should be used for all positional arguments. Lyra interprets
|
||||
// -u, --unknown_flag as positional argument. This validator makes sure that
|
||||
// a positional argument starting with - is reported as an error. Otherwise,
|
||||
// writes the value to |str|.
|
||||
std::function<void(const std::string&)> PosArgValidator(std::string* str);
|
||||
|
||||
protected:
|
||||
// Adds all optional and required arguments used by the command.
|
||||
// Called by Register().
|
||||
virtual void RegisterCommandLineFlags(lyra::command& cmd) = 0;
|
||||
|
||||
// Runs the command. Called by lyra::cli::parse() when this command is
|
||||
// actually triggered and all flags have been parsed successfully.
|
||||
virtual absl::Status Run() = 0;
|
||||
|
||||
private:
|
||||
// Called by lyra::cli::parse() after successfully parsing arguments. Catches
|
||||
// unknown arguments (Lyra interprets those as positional args, not as an
|
||||
// error!), and displays the help text if appropriate, otherwise calls Run().
|
||||
void CommandHandler(const lyra::group& g);
|
||||
|
||||
std::string name_;
|
||||
std::string help_;
|
||||
int* exit_code_ = nullptr;
|
||||
|
||||
bool show_help_ = false;
|
||||
|
||||
// Workaround for invalid args. Lyra doesn't interpret --invalid as invalid
|
||||
// argument, but as positional argument "--invalid".
|
||||
std::string invalid_arg_;
|
||||
|
||||
// Extraneous positional args. Gets reported as error if present.
|
||||
std::string extra_positional_arg_;
|
||||
|
||||
// Errors from parsing JEDEC sizes.
|
||||
// Works around Lyra not accepting errors from parsers.
|
||||
std::string jedec_parse_error_;
|
||||
};
|
||||
|
||||
} // namespace cdc_ft
|
||||
|
||||
#endif // CDC_STREAM_BASE_COMMAND_H_
|
||||
216
cdc_stream/cdc_fuse_manager.cc
Normal file
216
cdc_stream/cdc_fuse_manager.cc
Normal file
@@ -0,0 +1,216 @@
|
||||
// Copyright 2022 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "cdc_stream/cdc_fuse_manager.h"
|
||||
|
||||
#include "absl/strings/match.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "cdc_fuse_fs/constants.h"
|
||||
#include "common/gamelet_component.h"
|
||||
#include "common/log.h"
|
||||
#include "common/path.h"
|
||||
#include "common/status.h"
|
||||
#include "common/status_macros.h"
|
||||
|
||||
namespace cdc_ft {
|
||||
namespace {
|
||||
|
||||
constexpr char kFuseFilename[] = "cdc_fuse_fs";
|
||||
constexpr char kLibFuseFilename[] = "libfuse.so";
|
||||
constexpr char kFuseStdoutPrefix[] = "cdc_fuse_fs_stdout";
|
||||
constexpr char kRemoteToolsBinDir[] = "~/.cache/cdc-file-transfer/bin/";
|
||||
|
||||
// Cache directory on the gamelet to store data chunks.
|
||||
constexpr char kCacheDir[] = "~/.cache/cdc-file-transfer/chunks";
|
||||
|
||||
} // namespace
|
||||
|
||||
CdcFuseManager::CdcFuseManager(std::string instance,
|
||||
ProcessFactory* process_factory,
|
||||
RemoteUtil* remote_util)
|
||||
: instance_(std::move(instance)),
|
||||
process_factory_(process_factory),
|
||||
remote_util_(remote_util) {}
|
||||
|
||||
CdcFuseManager::~CdcFuseManager() = default;
|
||||
|
||||
absl::Status CdcFuseManager::Deploy() {
|
||||
assert(!fuse_process_);
|
||||
|
||||
LOG_INFO("Deploying FUSE...");
|
||||
|
||||
std::string exe_dir;
|
||||
RETURN_IF_ERROR(path::GetExeDir(&exe_dir), "Failed to get exe directory");
|
||||
|
||||
// Set the cwd to the exe dir and pass the filenames to scp. Otherwise, some
|
||||
// scp implementations can get confused and create the wrong remote filenames.
|
||||
path::SetCwd(exe_dir);
|
||||
|
||||
// Copy FUSE to the gamelet.
|
||||
LOG_DEBUG("Copying FUSE");
|
||||
RETURN_IF_ERROR(remote_util_->Scp({kFuseFilename, kLibFuseFilename},
|
||||
kRemoteToolsBinDir, /*compress=*/false),
|
||||
"Failed to copy FUSE to gamelet");
|
||||
LOG_DEBUG("Copying FUSE succeeded");
|
||||
|
||||
// Make FUSE executable. Note that sync does it automatically.
|
||||
LOG_DEBUG("Making FUSE executable");
|
||||
std::string remotePath = path::JoinUnix(kRemoteToolsBinDir, kFuseFilename);
|
||||
RETURN_IF_ERROR(remote_util_->Chmod("a+x", remotePath),
|
||||
"Failed to set executable flag on FUSE");
|
||||
LOG_DEBUG("Making FUSE succeeded");
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status CdcFuseManager::Start(const std::string& mount_dir,
|
||||
uint16_t local_port, uint16_t remote_port,
|
||||
int verbosity, bool debug,
|
||||
bool singlethreaded, bool enable_stats,
|
||||
bool check, uint64_t cache_capacity,
|
||||
uint32_t cleanup_timeout_sec,
|
||||
uint32_t access_idle_timeout_sec) {
|
||||
assert(!fuse_process_);
|
||||
|
||||
// Gather stats for the FUSE gamelet component to determine whether a
|
||||
// re-deploy is necessary.
|
||||
std::string exe_dir;
|
||||
RETURN_IF_ERROR(path::GetExeDir(&exe_dir), "Failed to get exe directory");
|
||||
std::vector<GameletComponent> components;
|
||||
absl::Status status =
|
||||
GameletComponent::Get({path::Join(exe_dir, kFuseFilename),
|
||||
path::Join(exe_dir, kLibFuseFilename)},
|
||||
&components);
|
||||
if (!status.ok()) {
|
||||
return absl::NotFoundError(absl::StrFormat(
|
||||
"Required gamelet component not found. Make sure the files %s and %s "
|
||||
"reside in the same folder as stadia_assets_stream_manager_v3.exe.",
|
||||
kFuseFilename, kLibFuseFilename));
|
||||
}
|
||||
std::string component_args = GameletComponent::ToCommandLineArgs(components);
|
||||
|
||||
// Build the remote command.
|
||||
std::string remotePath = path::JoinUnix(kRemoteToolsBinDir, kFuseFilename);
|
||||
std::string remote_command = absl::StrFormat(
|
||||
"mkdir -p %s; LD_LIBRARY_PATH=%s %s "
|
||||
"--instance=%s "
|
||||
"--components=%s --port=%i --cache_dir=%s "
|
||||
"--verbosity=%i --cleanup_timeout=%i --access_idle_timeout=%i --stats=%i "
|
||||
"--check=%i --cache_capacity=%u -- -o allow_root -o ro -o nonempty -o "
|
||||
"auto_unmount %s%s%s",
|
||||
kRemoteToolsBinDir, kRemoteToolsBinDir, remotePath,
|
||||
RemoteUtil::QuoteForSsh(instance_),
|
||||
RemoteUtil::QuoteForSsh(component_args), remote_port, kCacheDir,
|
||||
verbosity, cleanup_timeout_sec, access_idle_timeout_sec, enable_stats,
|
||||
check, cache_capacity, RemoteUtil::QuoteForSsh(mount_dir),
|
||||
debug ? " -d" : "", singlethreaded ? " -s" : "");
|
||||
|
||||
bool needs_deploy = false;
|
||||
RETURN_IF_ERROR(
|
||||
RunFuseProcess(local_port, remote_port, remote_command, &needs_deploy));
|
||||
if (needs_deploy) {
|
||||
// Deploy and try again.
|
||||
RETURN_IF_ERROR(Deploy());
|
||||
RETURN_IF_ERROR(
|
||||
RunFuseProcess(local_port, remote_port, remote_command, &needs_deploy));
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status CdcFuseManager::RunFuseProcess(uint16_t local_port,
|
||||
uint16_t remote_port,
|
||||
const std::string& remote_command,
|
||||
bool* needs_deploy) {
|
||||
assert(!fuse_process_);
|
||||
assert(needs_deploy);
|
||||
*needs_deploy = false;
|
||||
|
||||
LOG_DEBUG("Running FUSE process");
|
||||
ProcessStartInfo start_info =
|
||||
remote_util_->BuildProcessStartInfoForSshPortForwardAndCommand(
|
||||
local_port, remote_port, true, remote_command);
|
||||
start_info.name = kFuseFilename;
|
||||
|
||||
// Capture stdout to determine whether a deploy is required.
|
||||
fuse_stdout_.clear();
|
||||
fuse_startup_finished_ = false;
|
||||
start_info.stdout_handler = [this, needs_deploy](const char* data,
|
||||
size_t size) {
|
||||
return HandleFuseStdout(data, size, needs_deploy);
|
||||
};
|
||||
fuse_process_ = process_factory_->Create(start_info);
|
||||
RETURN_IF_ERROR(fuse_process_->Start(), "Failed to start FUSE process");
|
||||
LOG_DEBUG("FUSE process started. Waiting for startup to finish.");
|
||||
|
||||
// Run until process exits or startup finishes.
|
||||
auto startup_finished = [this]() { return fuse_startup_finished_.load(); };
|
||||
RETURN_IF_ERROR(fuse_process_->RunUntil(startup_finished),
|
||||
"Failed to run FUSE process");
|
||||
LOG_DEBUG("FUSE process startup complete.");
|
||||
|
||||
// If the FUSE process exited before it could perform its up-to-date check, it
|
||||
// most likely happens because the binary does not exist and needs to be
|
||||
// deployed.
|
||||
*needs_deploy |= !fuse_startup_finished_ && fuse_process_->HasExited() &&
|
||||
fuse_process_->ExitCode() != 0;
|
||||
if (*needs_deploy) {
|
||||
LOG_DEBUG("FUSE needs to be (re-)deployed.");
|
||||
fuse_process_.reset();
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status CdcFuseManager::Stop() {
|
||||
if (!fuse_process_) {
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
LOG_DEBUG("Terminating FUSE process");
|
||||
absl::Status status = fuse_process_->Terminate();
|
||||
fuse_process_.reset();
|
||||
return status;
|
||||
}
|
||||
|
||||
bool CdcFuseManager::IsHealthy() const {
|
||||
return fuse_process_ && !fuse_process_->HasExited();
|
||||
}
|
||||
|
||||
absl::Status CdcFuseManager::HandleFuseStdout(const char* data, size_t size,
|
||||
bool* needs_deploy) {
|
||||
assert(needs_deploy);
|
||||
|
||||
// Don't capture stdout beyond startup.
|
||||
if (!fuse_startup_finished_) {
|
||||
fuse_stdout_.append(data, size);
|
||||
// The gamelet component prints some magic strings to stdout to indicate
|
||||
// whether it's up-to-date.
|
||||
if (absl::StrContains(fuse_stdout_, kFuseUpToDate)) {
|
||||
fuse_startup_finished_ = true;
|
||||
} else if (absl::StrContains(fuse_stdout_, kFuseNotUpToDate)) {
|
||||
fuse_startup_finished_ = true;
|
||||
*needs_deploy = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!remote_util_->Quiet()) {
|
||||
// Forward to logging.
|
||||
return LogOutput(kFuseStdoutPrefix, data, size);
|
||||
}
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
} // namespace cdc_ft
|
||||
99
cdc_stream/cdc_fuse_manager.h
Normal file
99
cdc_stream/cdc_fuse_manager.h
Normal file
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright 2022 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef CDC_STREAM_CDC_FUSE_MANAGER_H_
|
||||
#define CDC_STREAM_CDC_FUSE_MANAGER_H_
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "common/remote_util.h"
|
||||
|
||||
namespace cdc_ft {
|
||||
|
||||
class Process;
|
||||
class ProcessFactory;
|
||||
class RemoteUtil;
|
||||
|
||||
// Manages the gamelet-side CDC FUSE filesystem process.
|
||||
class CdcFuseManager {
|
||||
public:
|
||||
CdcFuseManager(std::string instance, ProcessFactory* process_factory,
|
||||
RemoteUtil* remote_util);
|
||||
~CdcFuseManager();
|
||||
|
||||
CdcFuseManager(CdcFuseManager&) = delete;
|
||||
CdcFuseManager& operator=(CdcFuseManager&) = delete;
|
||||
|
||||
// Starts the CDC FUSE and establishes a reverse SSH tunnel from the gamelet's
|
||||
// |remote_port| to the workstation's |local_port|. Deploys the binary if
|
||||
// necessary.
|
||||
//
|
||||
// |mount_dir| is the remote directory where to mount the FUSE.
|
||||
// |verbosity| is the log verbosity used by the filesystem.
|
||||
// |debug| puts the filesystem into debug mode if set to true. This also
|
||||
// causes the process to run in the foreground, so that logs are piped through
|
||||
// SSH to stdout of the workstation process.
|
||||
// |singlethreaded| puts the filesystem into single-threaded mode if true.
|
||||
// |enable_stats| determines whether FUSE should send debug statistics.
|
||||
// |check| determines whether to execute FUSE consistency check.
|
||||
// |cache_capacity| defines the cache capacity in bytes.
|
||||
// |cleanup_timeout_sec| defines the data provider cleanup timeout in seconds.
|
||||
// |access_idle_timeout_sec| defines the number of seconds after which data
|
||||
// provider is considered to be access-idling.
|
||||
absl::Status Start(const std::string& mount_dir, uint16_t local_port,
|
||||
uint16_t remote_port, int verbosity, bool debug,
|
||||
bool singlethreaded, bool enable_stats, bool check,
|
||||
uint64_t cache_capacity, uint32_t cleanup_timeout_sec,
|
||||
uint32_t access_idle_timeout_sec);
|
||||
|
||||
// Stops the CDC FUSE.
|
||||
absl::Status Stop();
|
||||
|
||||
// Returns true if the FUSE process is running.
|
||||
bool IsHealthy() const;
|
||||
|
||||
private:
|
||||
// Runs the FUSE process on the gamelet from the given |remote_command| and
|
||||
// establishes a reverse SSH tunnel from the gamelet's |remote_port| to the
|
||||
// workstation's |local_port|.
|
||||
//
|
||||
// If the FUSE is not up-to-date or does not exist, sets |needs_deploy| to
|
||||
// true and returns OK. In that case, Deploy() needs to be called and the FUSE
|
||||
// process should be run again.
|
||||
absl::Status RunFuseProcess(uint16_t local_port, uint16_t remote_port,
|
||||
const std::string& remote_command,
|
||||
bool* needs_deploy);
|
||||
|
||||
// Deploys the gamelet components.
|
||||
absl::Status Deploy();
|
||||
|
||||
// Output handler for FUSE's stdout. Sets |needs_deploy| to true if the output
|
||||
// contains a magic marker to indicate that the binary has to be redeployed.
|
||||
// Called in a background thread.
|
||||
absl::Status HandleFuseStdout(const char* data, size_t size,
|
||||
bool* needs_deploy);
|
||||
|
||||
std::string instance_;
|
||||
ProcessFactory* const process_factory_;
|
||||
RemoteUtil* const remote_util_;
|
||||
|
||||
std::unique_ptr<Process> fuse_process_;
|
||||
std::string fuse_stdout_;
|
||||
std::atomic<bool> fuse_startup_finished_{false};
|
||||
};
|
||||
|
||||
} // namespace cdc_ft
|
||||
|
||||
#endif // CDC_STREAM_CDC_FUSE_MANAGER_H_
|
||||
89
cdc_stream/cdc_stream.vcxproj
Normal file
89
cdc_stream/cdc_stream.vcxproj
Normal file
@@ -0,0 +1,89 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>15.0</VCProjectVersion>
|
||||
<ProjectGuid>{84D81562-D66C-4A60-9F48-2696D7D81D26}</ProjectGuid>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<RootNamespace>cdc_rsync</RootNamespace>
|
||||
<WindowsTargetPlatformVersion Condition="$(VisualStudioVersion) == 15">$([Microsoft.Build.Utilities.ToolLocationHelper]::GetLatestSDKTargetPlatformVersion('Windows', '10.0'))</WindowsTargetPlatformVersion>
|
||||
<WindowsTargetPlatformVersion Condition="$(VisualStudioVersion) == 16">10.0</WindowsTargetPlatformVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>Makefile</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset Condition="$(VisualStudioVersion) == 15">v141</PlatformToolset>
|
||||
<PlatformToolset Condition="$(VisualStudioVersion) == 16">v142</PlatformToolset>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>Makefile</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset Condition="$(VisualStudioVersion) == 15">v141</PlatformToolset>
|
||||
<PlatformToolset Condition="$(VisualStudioVersion) == 16">v142</PlatformToolset>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="Shared">
|
||||
<Import Project="..\all_files.vcxitems" Label="Shared" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<OutDir>$(SolutionDir)bazel-out\x64_windows-dbg\bin\cdc_stream\</OutDir>
|
||||
<NMakePreprocessorDefinitions>UNICODE</NMakePreprocessorDefinitions>
|
||||
<AdditionalOptions>/std:c++17</AdditionalOptions>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<OutDir>$(SolutionDir)bazel-out\x64_windows-opt\bin\asset_stcdc_streamream_manager\</OutDir>
|
||||
<NMakePreprocessorDefinitions>UNICODE</NMakePreprocessorDefinitions>
|
||||
<AdditionalOptions>/std:c++17</AdditionalOptions>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\cdc_fuse_fs\cdc_fuse_fs.vcxproj">
|
||||
<Project>{a537310c-0571-43d5-b7fe-c867f702294f}</Project>
|
||||
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
|
||||
<LinkLibraryDependencies>false</LinkLibraryDependencies>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<!-- Prevent console from being closed -->
|
||||
<ItemDefinitionGroup>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<!-- Bazel setup -->
|
||||
<PropertyGroup>
|
||||
<BazelTargets>//cdc_stream</BazelTargets>
|
||||
<BazelOutputFile>cdc_stream.exe</BazelOutputFile>
|
||||
<BazelIncludePaths>..\;..\third_party\absl;..\bazel-cdc-file-transfer\external\com_github_jsoncpp\include;..\bazel-cdc-file-transfer\external\com_github_blake3\c;..\third_party\googletest\googletest\include;..\bazel-cdc-file-transfer\external\com_google_protobuf\src;..\bazel-cdc-file-transfer\external\com_github_grpc_grpc\include;..\bazel-out\x64_windows-dbg\bin;..\bazel-cdc-file-transfer\external\com_github_lyra\include;$(VC_IncludePath);$(WindowsSDK_IncludePath)</BazelIncludePaths>
|
||||
</PropertyGroup>
|
||||
<Import Project="..\NMakeBazelProject.targets" />
|
||||
<!-- For some reason, msbuild doesn't include this file, so copy it explicitly. -->
|
||||
<!-- TODO: Reenable once we can cross-compile these.
|
||||
<PropertyGroup>
|
||||
<CdcFuseFsFile>$(SolutionDir)bazel-out\k8-$(BazelCompilationMode)\bin\cdc_fuse_fs\cdc_fuse_fs</CdcFuseFsFile>
|
||||
<LibFuseFile>$(SolutionDir)bazel-out\k8-$(BazelCompilationMode)\bin\third_party\fuse\libfuse.so</LibFuseFile>
|
||||
</PropertyGroup>
|
||||
<Target Name="CopyCdcFuseFs" Inputs="$(CdcFuseFsFile)" Outputs="$(OutDir)cdc_fuse_fs" AfterTargets="Build">
|
||||
<Copy SourceFiles="$(CdcFuseFsFile)" DestinationFiles="$(OutDir)cdc_fuse_fs" />
|
||||
</Target>
|
||||
<Target Name="CopyLibFuse" Inputs="$(LibFuseFile)" Outputs="$(OutDir)libfuse.so" AfterTargets="Build">
|
||||
<Copy SourceFiles="$(LibFuseFile)" DestinationFiles="$(OutDir)libfuse.so" />
|
||||
</Target> -->
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
</ImportGroup>
|
||||
</Project>
|
||||
2
cdc_stream/cdc_stream.vcxproj.filters
Normal file
2
cdc_stream/cdc_stream.vcxproj.filters
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" />
|
||||
328
cdc_stream/grpc_asset_stream_server.cc
Normal file
328
cdc_stream/grpc_asset_stream_server.cc
Normal file
@@ -0,0 +1,328 @@
|
||||
// Copyright 2022 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "cdc_stream/grpc_asset_stream_server.h"
|
||||
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "absl/time/time.h"
|
||||
#include "common/grpc_status.h"
|
||||
#include "common/log.h"
|
||||
#include "common/path.h"
|
||||
#include "common/status.h"
|
||||
#include "common/status_macros.h"
|
||||
#include "data_store/data_store_reader.h"
|
||||
#include "grpcpp/grpcpp.h"
|
||||
#include "manifest/file_chunk_map.h"
|
||||
#include "proto/asset_stream_service.grpc.pb.h"
|
||||
|
||||
namespace cdc_ft {
|
||||
namespace {
|
||||
|
||||
using GetContentRequest = proto::GetContentRequest;
|
||||
using GetContentResponse = proto::GetContentResponse;
|
||||
using SendCachedContentIdsRequest = proto::SendCachedContentIdsRequest;
|
||||
using SendCachedContentIdsResponse = proto::SendCachedContentIdsResponse;
|
||||
using AssetStreamService = proto::AssetStreamService;
|
||||
|
||||
using GetManifestIdRequest = proto::GetManifestIdRequest;
|
||||
using GetManifestIdResponse = proto::GetManifestIdResponse;
|
||||
using AckManifestIdReceivedRequest = proto::AckManifestIdReceivedRequest;
|
||||
using AckManifestIdReceivedResponse = proto::AckManifestIdReceivedResponse;
|
||||
using ConfigStreamService = proto::ConfigStreamService;
|
||||
using ProcessAssetsRequest = proto::ProcessAssetsRequest;
|
||||
using ProcessAssetsResponse = proto::ProcessAssetsResponse;
|
||||
|
||||
} // namespace
|
||||
|
||||
class AssetStreamServiceImpl final : public AssetStreamService::Service {
|
||||
public:
|
||||
AssetStreamServiceImpl(std::string src_dir,
|
||||
DataStoreReader* data_store_reader,
|
||||
FileChunkMap* file_chunks, InstanceIdMap* instance_ids,
|
||||
ContentSentHandler content_sent)
|
||||
: src_dir_(std::move(src_dir)),
|
||||
data_store_reader_(data_store_reader),
|
||||
file_chunks_(file_chunks),
|
||||
started_(absl::Now()),
|
||||
instance_ids_(instance_ids),
|
||||
content_sent_(content_sent) {}
|
||||
|
||||
grpc::Status GetContent(grpc::ServerContext* context,
|
||||
const GetContentRequest* request,
|
||||
GetContentResponse* response) override {
|
||||
// See if this is a data chunk first. The hash lookup is faster than the
|
||||
// file lookup from the data store.
|
||||
std::string rel_path;
|
||||
uint64_t offset;
|
||||
size_t size;
|
||||
std::string instance_id = instance_ids_->Get(context->peer());
|
||||
|
||||
for (const ContentIdProto& id : request->id()) {
|
||||
uint32_t uint32_size;
|
||||
if (file_chunks_->Lookup(id, &rel_path, &offset, &uint32_size)) {
|
||||
size = uint32_size;
|
||||
// File data chunk.
|
||||
RETURN_GRPC_IF_ERROR(ReadFromFile(id, rel_path, offset, uint32_size,
|
||||
response->add_data()));
|
||||
file_chunks_->RecordStreamedChunk(id, request->thread_id());
|
||||
} else {
|
||||
// Manifest chunk.
|
||||
RETURN_GRPC_IF_ERROR(
|
||||
ReadFromDataStore(id, response->add_data(), &size));
|
||||
}
|
||||
if (content_sent_ != nullptr) {
|
||||
content_sent_(size, 1, instance_id);
|
||||
}
|
||||
}
|
||||
return grpc::Status::OK;
|
||||
}
|
||||
|
||||
grpc::Status SendCachedContentIds(
|
||||
grpc::ServerContext* context, const SendCachedContentIdsRequest* request,
|
||||
SendCachedContentIdsResponse* response) override {
|
||||
for (const ContentIdProto& id : request->id())
|
||||
file_chunks_->RecordCachedChunk(id);
|
||||
return grpc::Status::OK;
|
||||
}
|
||||
|
||||
private:
|
||||
absl::Status ReadFromFile(const ContentIdProto& id,
|
||||
const std::string& rel_path, uint64_t offset,
|
||||
uint32_t size, std::string* data) {
|
||||
std::string path = path::Join(src_dir_, rel_path);
|
||||
path::FixPathSeparators(&path);
|
||||
data->resize(size);
|
||||
size_t read_size;
|
||||
ASSIGN_OR_RETURN(
|
||||
read_size,
|
||||
path::ReadFile(path, const_cast<char*>(data->data()), offset, size),
|
||||
"Failed to read chunk '%s', file '%s', offset %d, size %d",
|
||||
ContentId::ToHexString(id), path, offset, size);
|
||||
|
||||
absl::Time now = absl::Now();
|
||||
LOG_VERBOSE("'%s', %d, '%s', '%s', %u, %u",
|
||||
absl::FormatTime("%H:%M:%S", now, absl::UTCTimeZone()),
|
||||
absl::ToInt64Milliseconds(now - started_),
|
||||
ContentId::ToHexString(id), path, offset, size);
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status ReadFromDataStore(const ContentIdProto& id, std::string* data,
|
||||
size_t* size) {
|
||||
Buffer buf;
|
||||
RETURN_IF_ERROR(data_store_reader_->Get(id, &buf),
|
||||
"Failed to read chunk '%s'", ContentId::ToHexString(id));
|
||||
|
||||
// TODO: Get rid of copy after the Buffer uses std::string.
|
||||
*data = std::string(buf.data(), buf.size());
|
||||
*size = buf.size();
|
||||
absl::Time now = absl::Now();
|
||||
LOG_VERBOSE("'%s', %d, '%s', %d",
|
||||
absl::FormatTime("%H:%M:%S", now, absl::UTCTimeZone()),
|
||||
absl::ToInt64Milliseconds(now - started_),
|
||||
ContentId::ToHexString(id), buf.size());
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
const std::string src_dir_;
|
||||
DataStoreReader* const data_store_reader_;
|
||||
FileChunkMap* const file_chunks_;
|
||||
const absl::Time started_;
|
||||
InstanceIdMap* instance_ids_;
|
||||
ContentSentHandler content_sent_;
|
||||
};
|
||||
|
||||
class ConfigStreamServiceImpl final : public ConfigStreamService::Service {
|
||||
public:
|
||||
ConfigStreamServiceImpl(InstanceIdMap* instance_ids,
|
||||
PrioritizeAssetsHandler prio_handler)
|
||||
: instance_ids_(instance_ids), prio_handler_(std::move(prio_handler)) {}
|
||||
~ConfigStreamServiceImpl() { Shutdown(); }
|
||||
|
||||
grpc::Status GetManifestId(
|
||||
grpc::ServerContext* context, const GetManifestIdRequest* request,
|
||||
::grpc::ServerWriter<GetManifestIdResponse>* stream) override {
|
||||
ContentIdProto local_id;
|
||||
bool running = true;
|
||||
do {
|
||||
// Shutdown happened.
|
||||
if (!WaitForUpdate(local_id)) {
|
||||
break;
|
||||
}
|
||||
LOG_INFO("Sending updated manifest id '%s' to the gamelet",
|
||||
ContentId::ToHexString(local_id));
|
||||
GetManifestIdResponse response;
|
||||
*response.mutable_id() = local_id;
|
||||
bool success = stream->Write(response);
|
||||
if (!success) {
|
||||
LOG_WARNING("Failed to send updated manifest id '%s'",
|
||||
ContentId::ToHexString(local_id));
|
||||
}
|
||||
absl::ReaderMutexLock lock(&mutex_);
|
||||
running = running_;
|
||||
} while (running);
|
||||
return grpc::Status::OK;
|
||||
}
|
||||
|
||||
grpc::Status AckManifestIdReceived(
|
||||
grpc::ServerContext* context, const AckManifestIdReceivedRequest* request,
|
||||
AckManifestIdReceivedResponse* response) override {
|
||||
// Associate the peer with the gamelet ID.
|
||||
instance_ids_->Set(context->peer(), request->gamelet_id());
|
||||
absl::MutexLock lock(&mutex_);
|
||||
acked_manifest_ids_[request->gamelet_id()] = request->manifest_id();
|
||||
return grpc::Status::OK;
|
||||
}
|
||||
|
||||
grpc::Status ProcessAssets(grpc::ServerContext* context,
|
||||
const ProcessAssetsRequest* request,
|
||||
ProcessAssetsResponse* response) override {
|
||||
if (!prio_handler_) return grpc::Status::OK;
|
||||
|
||||
std::vector<std::string> rel_paths;
|
||||
rel_paths.reserve(request->relative_paths().size());
|
||||
for (const std::string& rel_path : request->relative_paths()) {
|
||||
rel_paths.push_back(rel_path);
|
||||
}
|
||||
prio_handler_(std::move(rel_paths));
|
||||
return grpc::Status::OK;
|
||||
}
|
||||
|
||||
void SetManifestId(const ContentIdProto& id) ABSL_LOCKS_EXCLUDED(mutex_) {
|
||||
LOG_INFO("Updating manifest id '%s' in configuration service",
|
||||
ContentId::ToHexString(id));
|
||||
absl::MutexLock lock(&mutex_);
|
||||
id_ = id;
|
||||
}
|
||||
|
||||
absl::Status WaitForManifestAck(const std::string& instance,
|
||||
absl::Duration timeout) {
|
||||
absl::MutexLock lock(&mutex_);
|
||||
auto cond = [this, &instance]() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_) {
|
||||
AckedManifestIdsMap::iterator iter = acked_manifest_ids_.find(instance);
|
||||
return iter != acked_manifest_ids_.end() && id_ == iter->second;
|
||||
};
|
||||
|
||||
if (!mutex_.AwaitWithTimeout(absl::Condition(&cond), timeout)) {
|
||||
return absl::DeadlineExceededError(absl::StrFormat(
|
||||
"Instance '%s' did not acknowledge reception of manifest", instance));
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
void Shutdown() ABSL_LOCKS_EXCLUDED(mutex_) {
|
||||
absl::MutexLock lock(&mutex_);
|
||||
if (running_) {
|
||||
LOG_INFO("Shutting down configuration service");
|
||||
running_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
ContentIdProto GetStoredManifestId() const ABSL_LOCKS_EXCLUDED(mutex_) {
|
||||
absl::MutexLock lock(&mutex_);
|
||||
return id_;
|
||||
}
|
||||
|
||||
void SetPrioritizeAssetsHandler(PrioritizeAssetsHandler handler) {
|
||||
prio_handler_ = handler;
|
||||
}
|
||||
|
||||
private:
|
||||
// Returns false if the update process was cancelled.
|
||||
bool WaitForUpdate(ContentIdProto& local_id) ABSL_LOCKS_EXCLUDED(mutex_) {
|
||||
absl::MutexLock lock(&mutex_);
|
||||
auto cond = [&]() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_) {
|
||||
return !running_ || local_id != id_;
|
||||
};
|
||||
mutex_.Await(absl::Condition(&cond));
|
||||
local_id = id_;
|
||||
return running_;
|
||||
}
|
||||
|
||||
mutable absl::Mutex mutex_;
|
||||
ContentIdProto id_ ABSL_GUARDED_BY(mutex_);
|
||||
bool running_ ABSL_GUARDED_BY(mutex_) = true;
|
||||
InstanceIdMap* instance_ids_ = nullptr;
|
||||
PrioritizeAssetsHandler prio_handler_;
|
||||
|
||||
// Maps instance ids to the last acknowledged manifest id.
|
||||
using AckedManifestIdsMap = std::unordered_map<std::string, ContentIdProto>;
|
||||
AckedManifestIdsMap acked_manifest_ids_ ABSL_GUARDED_BY(mutex_);
|
||||
};
|
||||
|
||||
GrpcAssetStreamServer::GrpcAssetStreamServer(
|
||||
std::string src_dir, DataStoreReader* data_store_reader,
|
||||
FileChunkMap* file_chunks, ContentSentHandler content_sent,
|
||||
PrioritizeAssetsHandler prio_assets)
|
||||
: AssetStreamServer(src_dir, data_store_reader, file_chunks),
|
||||
asset_stream_service_(std::make_unique<AssetStreamServiceImpl>(
|
||||
std::move(src_dir), data_store_reader, file_chunks, &instance_ids_,
|
||||
content_sent)),
|
||||
config_stream_service_(std::make_unique<ConfigStreamServiceImpl>(
|
||||
&instance_ids_, std::move(prio_assets))) {}
|
||||
|
||||
GrpcAssetStreamServer::~GrpcAssetStreamServer() = default;
|
||||
|
||||
absl::Status GrpcAssetStreamServer::Start(int port) {
|
||||
assert(!server_);
|
||||
|
||||
std::string server_address = absl::StrFormat("localhost:%i", port);
|
||||
grpc::ServerBuilder builder;
|
||||
int selected_port = 0;
|
||||
builder.AddListeningPort(server_address, grpc::InsecureServerCredentials(),
|
||||
&selected_port);
|
||||
builder.RegisterService(asset_stream_service_.get());
|
||||
builder.RegisterService(config_stream_service_.get());
|
||||
server_ = builder.BuildAndStart();
|
||||
if (selected_port != port) {
|
||||
return MakeStatus(
|
||||
"Failed to start streaming server: Could not listen on port %i. Is the "
|
||||
"port in use?",
|
||||
port);
|
||||
}
|
||||
if (!server_) return MakeStatus("Failed to start streaming server");
|
||||
LOG_INFO("Streaming server listening on '%s'", server_address);
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
void GrpcAssetStreamServer::SetManifestId(const ContentIdProto& manifest_id) {
|
||||
LOG_INFO("Setting manifest id '%s'", ContentId::ToHexString(manifest_id));
|
||||
assert(config_stream_service_);
|
||||
config_stream_service_->SetManifestId(manifest_id);
|
||||
}
|
||||
|
||||
absl::Status GrpcAssetStreamServer::WaitForManifestAck(
|
||||
const std::string& instance, absl::Duration timeout) {
|
||||
assert(config_stream_service_);
|
||||
return config_stream_service_->WaitForManifestAck(instance, timeout);
|
||||
}
|
||||
|
||||
void GrpcAssetStreamServer::Shutdown() {
|
||||
assert(config_stream_service_);
|
||||
config_stream_service_->Shutdown();
|
||||
if (server_) {
|
||||
server_->Shutdown();
|
||||
server_->Wait();
|
||||
}
|
||||
}
|
||||
|
||||
ContentIdProto GrpcAssetStreamServer::GetManifestId() const {
|
||||
assert(config_stream_service_);
|
||||
return config_stream_service_->GetStoredManifestId();
|
||||
}
|
||||
|
||||
} // namespace cdc_ft
|
||||
70
cdc_stream/grpc_asset_stream_server.h
Normal file
70
cdc_stream/grpc_asset_stream_server.h
Normal 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 CDC_STREAM_GRPC_ASSET_STREAM_SERVER_H_
|
||||
#define CDC_STREAM_GRPC_ASSET_STREAM_SERVER_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "cdc_stream/asset_stream_server.h"
|
||||
#include "common/thread_safe_map.h"
|
||||
|
||||
namespace grpc {
|
||||
class Server;
|
||||
}
|
||||
|
||||
namespace cdc_ft {
|
||||
|
||||
using InstanceIdMap = ThreadSafeMap<std::string, std::string>;
|
||||
|
||||
class AssetStreamServiceImpl;
|
||||
class ConfigStreamServiceImpl;
|
||||
|
||||
// gRpc server for streaming assets to one or more gamelets.
|
||||
class GrpcAssetStreamServer : public AssetStreamServer {
|
||||
public:
|
||||
// Creates a new asset streaming gRpc server.
|
||||
GrpcAssetStreamServer(std::string src_dir, DataStoreReader* data_store_reader,
|
||||
FileChunkMap* file_chunks,
|
||||
ContentSentHandler content_sent,
|
||||
PrioritizeAssetsHandler prio_assets);
|
||||
|
||||
~GrpcAssetStreamServer();
|
||||
|
||||
// AssetStreamServer:
|
||||
|
||||
absl::Status Start(int port) override;
|
||||
|
||||
void SetManifestId(const ContentIdProto& manifest_id) override;
|
||||
|
||||
absl::Status WaitForManifestAck(const std::string& instance,
|
||||
absl::Duration timeout) override;
|
||||
|
||||
void Shutdown() override;
|
||||
|
||||
ContentIdProto GetManifestId() const override;
|
||||
|
||||
private:
|
||||
InstanceIdMap instance_ids_;
|
||||
const std::unique_ptr<AssetStreamServiceImpl> asset_stream_service_;
|
||||
const std::unique_ptr<ConfigStreamServiceImpl> config_stream_service_;
|
||||
std::unique_ptr<grpc::Server> server_;
|
||||
};
|
||||
|
||||
} // namespace cdc_ft
|
||||
|
||||
#endif // CDC_STREAM_GRPC_ASSET_STREAM_SERVER_H_
|
||||
95
cdc_stream/local_assets_stream_manager_client.cc
Normal file
95
cdc_stream/local_assets_stream_manager_client.cc
Normal file
@@ -0,0 +1,95 @@
|
||||
// Copyright 2022 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "cdc_stream/local_assets_stream_manager_client.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "absl/strings/str_split.h"
|
||||
#include "common/grpc_status.h"
|
||||
|
||||
namespace cdc_ft {
|
||||
|
||||
using StartSessionRequest = localassetsstreammanager::StartSessionRequest;
|
||||
using StartSessionResponse = localassetsstreammanager::StartSessionResponse;
|
||||
using StopSessionRequest = localassetsstreammanager::StopSessionRequest;
|
||||
using StopSessionResponse = localassetsstreammanager::StopSessionResponse;
|
||||
|
||||
LocalAssetsStreamManagerClient::LocalAssetsStreamManagerClient(
|
||||
uint16_t service_port) {
|
||||
std::string client_address = absl::StrFormat("localhost:%u", service_port);
|
||||
std::shared_ptr<grpc::Channel> channel = grpc::CreateCustomChannel(
|
||||
client_address, grpc::InsecureChannelCredentials(),
|
||||
grpc::ChannelArguments());
|
||||
stub_ = LocalAssetsStreamManager::NewStub(std::move(channel));
|
||||
}
|
||||
|
||||
LocalAssetsStreamManagerClient::LocalAssetsStreamManagerClient(
|
||||
std::shared_ptr<grpc::Channel> channel) {
|
||||
stub_ = LocalAssetsStreamManager::NewStub(std::move(channel));
|
||||
}
|
||||
|
||||
LocalAssetsStreamManagerClient::~LocalAssetsStreamManagerClient() = default;
|
||||
|
||||
absl::Status LocalAssetsStreamManagerClient::StartSession(
|
||||
const std::string& src_dir, const std::string& user_host, uint16_t ssh_port,
|
||||
const std::string& mount_dir, const std::string& ssh_command,
|
||||
const std::string& scp_command) {
|
||||
StartSessionRequest request;
|
||||
request.set_workstation_directory(src_dir);
|
||||
request.set_user_host(user_host);
|
||||
request.set_port(ssh_port);
|
||||
request.set_mount_dir(mount_dir);
|
||||
request.set_ssh_command(ssh_command);
|
||||
request.set_scp_command(scp_command);
|
||||
|
||||
grpc::ClientContext context;
|
||||
StartSessionResponse response;
|
||||
return ToAbslStatus(stub_->StartSession(&context, request, &response));
|
||||
}
|
||||
|
||||
absl::Status LocalAssetsStreamManagerClient::StopSession(
|
||||
const std::string& user_host, const std::string& mount_dir) {
|
||||
StopSessionRequest request;
|
||||
request.set_user_host(user_host);
|
||||
request.set_mount_dir(mount_dir);
|
||||
|
||||
grpc::ClientContext context;
|
||||
StopSessionResponse response;
|
||||
return ToAbslStatus(stub_->StopSession(&context, request, &response));
|
||||
}
|
||||
|
||||
// static
|
||||
absl::Status LocalAssetsStreamManagerClient::ParseUserHostDir(
|
||||
const std::string& user_host_dir, std::string* user_host,
|
||||
std::string* dir) {
|
||||
std::vector<std::string> parts =
|
||||
absl::StrSplit(user_host_dir, absl::MaxSplits(':', 1));
|
||||
if (parts.size() < 2 ||
|
||||
(parts[0].size() == 1 && toupper(parts[0][0]) >= 'A' &&
|
||||
toupper(parts[0][0]) <= 'Z')) {
|
||||
return absl::InvalidArgumentError(
|
||||
absl::StrFormat("Failed to parse '%s'. Make sure it is of the form "
|
||||
"[user@]host:linux_dir.",
|
||||
user_host_dir));
|
||||
}
|
||||
|
||||
*user_host = parts[0];
|
||||
*dir = parts[1];
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
} // namespace cdc_ft
|
||||
77
cdc_stream/local_assets_stream_manager_client.h
Normal file
77
cdc_stream/local_assets_stream_manager_client.h
Normal 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.
|
||||
*/
|
||||
|
||||
#ifndef CDC_STREAM_LOCAL_ASSETS_STREAM_MANAGER_CLIENT_H_
|
||||
#define CDC_STREAM_LOCAL_ASSETS_STREAM_MANAGER_CLIENT_H_
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "grpcpp/channel.h"
|
||||
#include "proto/local_assets_stream_manager.grpc.pb.h"
|
||||
|
||||
namespace grpc_impl {
|
||||
class Channel;
|
||||
}
|
||||
|
||||
namespace cdc_ft {
|
||||
|
||||
// gRpc client for starting/stopping asset streaming sessions.
|
||||
class LocalAssetsStreamManagerClient {
|
||||
public:
|
||||
explicit LocalAssetsStreamManagerClient(uint16_t service_port);
|
||||
|
||||
// |channel| is a grpc channel to use.
|
||||
explicit LocalAssetsStreamManagerClient(
|
||||
std::shared_ptr<grpc::Channel> channel);
|
||||
|
||||
~LocalAssetsStreamManagerClient();
|
||||
|
||||
// Starts streaming |src_dir| to |user_host|:|mount_dir|.
|
||||
// Starting a second session to the same target will stop the first one.
|
||||
// |src_dir| is the Windows source directory to stream.
|
||||
// |user_host| is the Linux host, formatted as [user@:host].
|
||||
// |ssh_port| is the SSH port to use while connecting to the host.
|
||||
// |mount_dir| is the Linux target directory to stream to.
|
||||
// |ssh_command| is the ssh command and extra arguments to use.
|
||||
// |scp_command| is the scp command and extra arguments to use.
|
||||
absl::Status StartSession(const std::string& src_dir,
|
||||
const std::string& user_host, uint16_t ssh_port,
|
||||
const std::string& mount_dir,
|
||||
const std::string& ssh_command,
|
||||
const std::string& scp_command);
|
||||
|
||||
// Stops the streaming session to the Linux target |user_host|:|mount_dir|.
|
||||
// |user_host| is the Linux host, formatted as [user@:host].
|
||||
// |mount_dir| is the Linux target directory.
|
||||
absl::Status StopSession(const std::string& user_host,
|
||||
const std::string& mount_dir);
|
||||
|
||||
// Helper function that splits "user@host:dir" into "user@host" and "dir".
|
||||
// Does not think that C: is a host.
|
||||
static absl::Status ParseUserHostDir(const std::string& user_host_dir,
|
||||
std::string* user_host,
|
||||
std::string* dir);
|
||||
|
||||
private:
|
||||
using LocalAssetsStreamManager =
|
||||
localassetsstreammanager::LocalAssetsStreamManager;
|
||||
std::unique_ptr<LocalAssetsStreamManager::Stub> stub_;
|
||||
};
|
||||
|
||||
} // namespace cdc_ft
|
||||
|
||||
#endif // CDC_STREAM_LOCAL_ASSETS_STREAM_MANAGER_CLIENT_H_
|
||||
325
cdc_stream/local_assets_stream_manager_service_impl.cc
Normal file
325
cdc_stream/local_assets_stream_manager_service_impl.cc
Normal file
@@ -0,0 +1,325 @@
|
||||
// Copyright 2022 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "cdc_stream/local_assets_stream_manager_service_impl.h"
|
||||
|
||||
#include <iomanip>
|
||||
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "absl/strings/str_replace.h"
|
||||
#include "absl/strings/str_split.h"
|
||||
#include "cdc_stream/multi_session.h"
|
||||
#include "cdc_stream/session_manager.h"
|
||||
#include "common/grpc_status.h"
|
||||
#include "common/log.h"
|
||||
#include "common/path.h"
|
||||
#include "common/process.h"
|
||||
#include "common/sdk_util.h"
|
||||
#include "common/status.h"
|
||||
#include "google/protobuf/text_format.h"
|
||||
#include "manifest/manifest_updater.h"
|
||||
|
||||
using TextFormat = google::protobuf::TextFormat;
|
||||
|
||||
namespace cdc_ft {
|
||||
namespace {
|
||||
|
||||
std::string RequestToString(const google::protobuf::Message& request) {
|
||||
std::string str;
|
||||
google::protobuf::TextFormat::PrintToString(request, &str);
|
||||
if (!str.empty() && str.back() == '\n') str.pop_back();
|
||||
return absl::StrReplaceAll(str, {{"\n", ", "}});
|
||||
}
|
||||
|
||||
// Parses |instance_name| of the form
|
||||
// "organizations/{org-id}/projects/{proj-id}/pools/{pool-id}/gamelets/{gamelet-id}"
|
||||
// into parts. The pool id is not returned.
|
||||
bool ParseInstanceName(const std::string& instance_name,
|
||||
std::string* instance_id, std::string* project_id,
|
||||
std::string* organization_id) {
|
||||
std::string pool_id;
|
||||
std::vector<std::string> parts = absl::StrSplit(instance_name, '/');
|
||||
if (parts.size() != 10) return false;
|
||||
if (parts[0] != "organizations" || parts[1].empty()) return false;
|
||||
if (parts[2] != "projects" || parts[3].empty()) return false;
|
||||
if (parts[4] != "pools" || parts[5].empty()) return false;
|
||||
// Instance id is e.g.
|
||||
// edge/e-europe-west3-b/49d010c7be1845ac9a19a9033c64a460ces1
|
||||
if (parts[6] != "gamelets" || parts[7].empty() || parts[8].empty() ||
|
||||
parts[9].empty())
|
||||
return false;
|
||||
*organization_id = parts[1];
|
||||
*project_id = parts[3];
|
||||
*instance_id = absl::StrFormat("%s/%s/%s", parts[7], parts[8], parts[9]);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parses |data| line by line for "|key|: value" and puts the first instance in
|
||||
// |value| if present. Returns false if |data| does not contain "|key|: value".
|
||||
// Trims whitespace.
|
||||
bool ParseValue(const std::string& data, const std::string& key,
|
||||
std::string* value) {
|
||||
std::istringstream stream(data);
|
||||
|
||||
std::string line;
|
||||
while (std::getline(stream, line)) {
|
||||
if (line.find(key + ":") == 0) {
|
||||
// Trim value.
|
||||
size_t start_pos = key.size() + 1;
|
||||
while (start_pos < line.size() && isspace(line[start_pos])) {
|
||||
start_pos++;
|
||||
}
|
||||
size_t end_pos = line.size();
|
||||
while (end_pos > start_pos && isspace(line[end_pos - 1])) {
|
||||
end_pos--;
|
||||
}
|
||||
*value = line.substr(start_pos, end_pos - start_pos);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Why oh why?
|
||||
std::string Quoted(const std::string& s) {
|
||||
std::ostringstream ss;
|
||||
ss << std::quoted(s);
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
LocalAssetsStreamManagerServiceImpl::LocalAssetsStreamManagerServiceImpl(
|
||||
SessionManager* session_manager, ProcessFactory* process_factory,
|
||||
metrics::MetricsService* metrics_service)
|
||||
: session_manager_(session_manager),
|
||||
process_factory_(process_factory),
|
||||
metrics_service_(metrics_service) {}
|
||||
|
||||
LocalAssetsStreamManagerServiceImpl::~LocalAssetsStreamManagerServiceImpl() =
|
||||
default;
|
||||
|
||||
grpc::Status LocalAssetsStreamManagerServiceImpl::StartSession(
|
||||
grpc::ServerContext* /*context*/, const StartSessionRequest* request,
|
||||
StartSessionResponse* /*response*/) {
|
||||
LOG_INFO("RPC:StartSession(%s)", RequestToString(*request));
|
||||
|
||||
MultiSession* ms = nullptr;
|
||||
metrics::DeveloperLogEvent evt;
|
||||
std::string instance_id;
|
||||
absl::Status status = StartSessionInternal(request, &instance_id, &ms, &evt);
|
||||
|
||||
evt.as_manager_data->session_start_data->absl_status = status.code();
|
||||
if (ms) {
|
||||
evt.as_manager_data->session_start_data->concurrent_session_count =
|
||||
ms->GetSessionCount();
|
||||
if (!instance_id.empty() && ms->HasSession(instance_id)) {
|
||||
ms->RecordSessionEvent(std::move(evt), metrics::EventType::kSessionStart,
|
||||
instance_id);
|
||||
} else {
|
||||
ms->RecordMultiSessionEvent(std::move(evt),
|
||||
metrics::EventType::kSessionStart);
|
||||
}
|
||||
} else {
|
||||
metrics_service_->RecordEvent(std::move(evt),
|
||||
metrics::EventType::kSessionStart);
|
||||
}
|
||||
|
||||
if (status.ok()) {
|
||||
LOG_INFO("StartSession() succeeded");
|
||||
} else {
|
||||
LOG_ERROR("StartSession() failed: %s", status.ToString());
|
||||
}
|
||||
return ToGrpcStatus(status);
|
||||
}
|
||||
|
||||
grpc::Status LocalAssetsStreamManagerServiceImpl::StopSession(
|
||||
grpc::ServerContext* /*context*/, const StopSessionRequest* request,
|
||||
StopSessionResponse* /*response*/) {
|
||||
LOG_INFO("RPC:StopSession(%s)", RequestToString(*request));
|
||||
|
||||
std::string instance_id =
|
||||
!request->gamelet_id().empty() // Stadia use case
|
||||
? request->gamelet_id()
|
||||
: absl::StrCat(request->user_host(), ":", request->mount_dir());
|
||||
|
||||
absl::Status status = session_manager_->StopSession(instance_id);
|
||||
if (status.ok()) {
|
||||
LOG_INFO("StopSession() succeeded");
|
||||
} else {
|
||||
LOG_ERROR("StopSession() failed: %s", status.ToString());
|
||||
}
|
||||
return ToGrpcStatus(status);
|
||||
}
|
||||
|
||||
absl::Status LocalAssetsStreamManagerServiceImpl::StartSessionInternal(
|
||||
const StartSessionRequest* request, std::string* instance_id,
|
||||
MultiSession** ms, metrics::DeveloperLogEvent* evt) {
|
||||
instance_id->clear();
|
||||
*ms = nullptr;
|
||||
evt->as_manager_data = std::make_unique<metrics::AssetStreamingManagerData>();
|
||||
evt->as_manager_data->session_start_data =
|
||||
std::make_unique<metrics::SessionStartData>();
|
||||
evt->as_manager_data->session_start_data->absl_status = absl::StatusCode::kOk;
|
||||
evt->as_manager_data->session_start_data->status =
|
||||
metrics::SessionStartStatus::kOk;
|
||||
evt->as_manager_data->session_start_data->origin =
|
||||
ConvertOrigin(request->origin());
|
||||
|
||||
if (!(request->gamelet_name().empty() ^ request->user_host().empty())) {
|
||||
return absl::InvalidArgumentError(
|
||||
"Must set either gamelet_name or user_host.");
|
||||
}
|
||||
|
||||
if (request->mount_dir().empty()) {
|
||||
return absl::InvalidArgumentError("mount_dir cannot be empty.");
|
||||
}
|
||||
|
||||
SessionTarget target;
|
||||
if (!request->gamelet_name().empty()) {
|
||||
ASSIGN_OR_RETURN(target,
|
||||
GetTargetForStadia(*request, instance_id, &evt->project_id,
|
||||
&evt->organization_id));
|
||||
} else {
|
||||
target = GetTarget(*request, instance_id);
|
||||
}
|
||||
|
||||
return session_manager_->StartSession(
|
||||
*instance_id, request->workstation_directory(), target, evt->project_id,
|
||||
evt->organization_id, ms,
|
||||
&evt->as_manager_data->session_start_data->status);
|
||||
}
|
||||
|
||||
absl::StatusOr<SessionTarget>
|
||||
LocalAssetsStreamManagerServiceImpl::GetTargetForStadia(
|
||||
const StartSessionRequest& request, std::string* instance_id,
|
||||
std::string* project_id, std::string* organization_id) {
|
||||
SessionTarget target;
|
||||
target.mount_dir = request.mount_dir();
|
||||
target.ssh_command = request.ssh_command();
|
||||
target.scp_command = request.scp_command();
|
||||
|
||||
// Parse instance/project/org id.
|
||||
if (!ParseInstanceName(request.gamelet_name(), instance_id, project_id,
|
||||
organization_id)) {
|
||||
return absl::InvalidArgumentError(absl::StrFormat(
|
||||
"Failed to parse instance name '%s'", request.gamelet_name()));
|
||||
}
|
||||
|
||||
// Run 'ggp ssh init' to determine IP (host) and port.
|
||||
std::string instance_ip;
|
||||
uint16_t instance_port = 0;
|
||||
RETURN_IF_ERROR(InitSsh(*instance_id, *project_id, *organization_id,
|
||||
&instance_ip, &instance_port));
|
||||
|
||||
target.user_host = "cloudcast@" + instance_ip;
|
||||
target.ssh_port = instance_port;
|
||||
return target;
|
||||
}
|
||||
|
||||
SessionTarget LocalAssetsStreamManagerServiceImpl::GetTarget(
|
||||
const StartSessionRequest& request, std::string* instance_id) {
|
||||
SessionTarget target;
|
||||
target.user_host = request.user_host();
|
||||
target.mount_dir = request.mount_dir();
|
||||
target.ssh_command = request.ssh_command();
|
||||
target.scp_command = request.scp_command();
|
||||
target.ssh_port = request.port() > 0 && request.port() <= UINT16_MAX
|
||||
? static_cast<uint16_t>(request.port())
|
||||
: RemoteUtil::kDefaultSshPort;
|
||||
|
||||
*instance_id = absl::StrCat(target.user_host, ":", target.mount_dir);
|
||||
return target;
|
||||
}
|
||||
|
||||
metrics::RequestOrigin LocalAssetsStreamManagerServiceImpl::ConvertOrigin(
|
||||
StartSessionRequestOrigin origin) const {
|
||||
switch (origin) {
|
||||
case StartSessionRequest::ORIGIN_UNKNOWN:
|
||||
return metrics::RequestOrigin::kUnknown;
|
||||
case StartSessionRequest::ORIGIN_CLI:
|
||||
return metrics::RequestOrigin::kCli;
|
||||
case StartSessionRequest::ORIGIN_PARTNER_PORTAL:
|
||||
return metrics::RequestOrigin::kPartnerPortal;
|
||||
default:
|
||||
return metrics::RequestOrigin::kUnknown;
|
||||
}
|
||||
}
|
||||
|
||||
absl::Status LocalAssetsStreamManagerServiceImpl::InitSsh(
|
||||
const std::string& instance_id, const std::string& project_id,
|
||||
const std::string& organization_id, std::string* instance_ip,
|
||||
uint16_t* instance_port) {
|
||||
SdkUtil sdk_util;
|
||||
instance_ip->clear();
|
||||
*instance_port = 0;
|
||||
|
||||
ProcessStartInfo start_info;
|
||||
start_info.command = absl::StrFormat(
|
||||
"%s ssh init", path::Join(sdk_util.GetDevBinPath(), "ggp"));
|
||||
start_info.command += absl::StrFormat(" --instance %s", Quoted(instance_id));
|
||||
if (!project_id.empty()) {
|
||||
start_info.command += absl::StrFormat(" --project %s", Quoted(project_id));
|
||||
}
|
||||
if (!organization_id.empty()) {
|
||||
start_info.command +=
|
||||
absl::StrFormat(" --organization %s", Quoted(organization_id));
|
||||
}
|
||||
start_info.name = "ggp ssh init";
|
||||
|
||||
std::string output;
|
||||
start_info.stdout_handler = [&output, this](const char* data,
|
||||
size_t data_size) {
|
||||
// Note: This is called from a background thread!
|
||||
output.append(data, data_size);
|
||||
return absl::OkStatus();
|
||||
};
|
||||
start_info.forward_output_to_log = true;
|
||||
|
||||
std::unique_ptr<Process> process = process_factory_->Create(start_info);
|
||||
absl::Status status = process->Start();
|
||||
if (!status.ok()) {
|
||||
return WrapStatus(status, "Failed to start ggp process");
|
||||
}
|
||||
|
||||
status = process->RunUntilExit();
|
||||
if (!status.ok()) {
|
||||
return WrapStatus(status, "Failed to run ggp process");
|
||||
}
|
||||
|
||||
uint32_t exit_code = process->ExitCode();
|
||||
if (exit_code != 0) {
|
||||
return MakeStatus("ggp process exited with code %u", exit_code);
|
||||
}
|
||||
|
||||
// Parse gamelet IP. Should be "Host: <instance_ip ip>".
|
||||
if (!ParseValue(output, "Host", instance_ip)) {
|
||||
return MakeStatus("Failed to parse host from ggp ssh init response\n%s",
|
||||
output);
|
||||
}
|
||||
|
||||
// Parse ssh port. Should be "Port: <port>".
|
||||
std::string port_string;
|
||||
const bool result = ParseValue(output, "Port", &port_string);
|
||||
int int_port = atoi(port_string.c_str());
|
||||
if (!result || int_port == 0 || int_port <= 0 || int_port > UINT_MAX) {
|
||||
return MakeStatus("Failed to parse ssh port from ggp ssh init response\n%s",
|
||||
output);
|
||||
}
|
||||
|
||||
*instance_port = static_cast<uint16_t>(int_port);
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
} // namespace cdc_ft
|
||||
110
cdc_stream/local_assets_stream_manager_service_impl.h
Normal file
110
cdc_stream/local_assets_stream_manager_service_impl.h
Normal file
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright 2022 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef CDC_STREAM_LOCAL_ASSETS_STREAM_MANAGER_SERVICE_IMPL_H_
|
||||
#define CDC_STREAM_LOCAL_ASSETS_STREAM_MANAGER_SERVICE_IMPL_H_
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "cdc_stream/session.h"
|
||||
#include "cdc_stream/session_config.h"
|
||||
#include "metrics/metrics.h"
|
||||
#include "proto/local_assets_stream_manager.grpc.pb.h"
|
||||
|
||||
namespace cdc_ft {
|
||||
|
||||
class MultiSession;
|
||||
class ProcessFactory;
|
||||
class SessionManager;
|
||||
|
||||
// Implements a service to start and stop streaming sessions as a server.
|
||||
// The corresponding clients are implemented by the ggp CLI and SDK Proxy.
|
||||
// The CLI triggers StartSession() from `ggp instance mount --local-dir` and
|
||||
// StopSession() from `ggp instance unmount`. SDK Proxy invokes StartSession()
|
||||
// when a user starts a new game from the partner portal and sets an `Asset
|
||||
// streaming directory` in the `Advanced settings` in the `Play settings`
|
||||
// dialog.
|
||||
// This service is owned by SessionManagementServer.
|
||||
class LocalAssetsStreamManagerServiceImpl final
|
||||
: public localassetsstreammanager::LocalAssetsStreamManager::Service {
|
||||
public:
|
||||
using StartSessionRequest = localassetsstreammanager::StartSessionRequest;
|
||||
using StartSessionRequestOrigin =
|
||||
localassetsstreammanager::StartSessionRequest_Origin;
|
||||
using StartSessionResponse = localassetsstreammanager::StartSessionResponse;
|
||||
using StopSessionRequest = localassetsstreammanager::StopSessionRequest;
|
||||
using StopSessionResponse = localassetsstreammanager::StopSessionResponse;
|
||||
|
||||
LocalAssetsStreamManagerServiceImpl(
|
||||
SessionManager* session_manager, ProcessFactory* process_factory,
|
||||
metrics::MetricsService* const metrics_service);
|
||||
~LocalAssetsStreamManagerServiceImpl();
|
||||
|
||||
// Starts a streaming session from path |request->workstation_directory()| to
|
||||
// the instance with id |request->gamelet_id()|. Stops an existing session
|
||||
// if it exists.
|
||||
grpc::Status StartSession(grpc::ServerContext* context,
|
||||
const StartSessionRequest* request,
|
||||
StartSessionResponse* response) override;
|
||||
|
||||
// Stops the streaming session to the instance with id
|
||||
// |request->gamelet_id()|. Returns a NotFound error if no session exists.
|
||||
grpc::Status StopSession(grpc::ServerContext* context,
|
||||
const StopSessionRequest* request,
|
||||
StopSessionResponse* response) override;
|
||||
|
||||
private:
|
||||
// Internal implementation of StartSession(). Returns the unique session
|
||||
// identifier |instance_id|, the created or retrieved MultiSession |ms| as
|
||||
// well as the filled metrics event |evt|.
|
||||
absl::Status LocalAssetsStreamManagerServiceImpl::StartSessionInternal(
|
||||
const StartSessionRequest* request, std::string* instance_id,
|
||||
MultiSession** ms, metrics::DeveloperLogEvent* evt);
|
||||
|
||||
// Stadia-specific: Returns a SessionTarget from a gamelet name and fills in
|
||||
// the gamelet's |instance_id|, |project_id| and |organization_id|.
|
||||
// Used if request.gamelet_name() is set.
|
||||
// Fails if the gamelet name fails to parse or if ggp ssh init fails.
|
||||
absl::StatusOr<SessionTarget> GetTargetForStadia(
|
||||
const StartSessionRequest& request, std::string* instance_id,
|
||||
std::string* project_id, std::string* organization_id);
|
||||
|
||||
// Returns a SessionTarget from the corresponding fields in |request|.
|
||||
// |instance_id| is set to [user@]host:mount_dir.
|
||||
// Used if request.gamelet_name() is not set.
|
||||
SessionTarget GetTarget(const StartSessionRequest& request,
|
||||
std::string* instance_id);
|
||||
|
||||
// Convert StartSessionRequest enum to metrics enum.
|
||||
metrics::RequestOrigin ConvertOrigin(StartSessionRequestOrigin origin) const;
|
||||
|
||||
// Initializes an ssh connection to a gamelet by calling 'ggp ssh init'.
|
||||
// |instance_id| must be set, |project_id|, |organization_id| are optional.
|
||||
// Returns |instance_ip| and |instance_port| (SSH port).
|
||||
absl::Status InitSsh(const std::string& instance_id,
|
||||
const std::string& project_id,
|
||||
const std::string& organization_id,
|
||||
std::string* instance_ip, uint16_t* instance_port);
|
||||
|
||||
const SessionConfig cfg_;
|
||||
SessionManager* const session_manager_;
|
||||
ProcessFactory* const process_factory_;
|
||||
metrics::MetricsService* const metrics_service_;
|
||||
};
|
||||
|
||||
} // namespace cdc_ft
|
||||
|
||||
#endif // CDC_STREAM_LOCAL_ASSETS_STREAM_MANAGER_SERVICE_IMPL_H_
|
||||
51
cdc_stream/main.cc
Normal file
51
cdc_stream/main.cc
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright 2022 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "cdc_stream/start_command.h"
|
||||
#include "cdc_stream/start_service_command.h"
|
||||
#include "cdc_stream/stop_command.h"
|
||||
#include "lyra/lyra.hpp"
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
// Set up commands.
|
||||
auto cli = lyra::cli();
|
||||
bool show_help = false;
|
||||
int exit_code = -1;
|
||||
cli.add_argument(lyra::help(show_help));
|
||||
|
||||
cdc_ft::StartCommand start_cmd(&exit_code);
|
||||
start_cmd.Register(cli);
|
||||
|
||||
cdc_ft::StopCommand stop_cmd(&exit_code);
|
||||
stop_cmd.Register(cli);
|
||||
|
||||
cdc_ft::StartServiceCommand start_service_cmd(&exit_code);
|
||||
start_service_cmd.Register(cli);
|
||||
|
||||
// Parse args and run. Note that parse actually runs the commands.
|
||||
// exit_code is -1 if no command was run.
|
||||
auto result = cli.parse({argc, argv});
|
||||
if (show_help || exit_code == -1) {
|
||||
std::cout << cli;
|
||||
return 0;
|
||||
}
|
||||
if (!result) {
|
||||
// Parse error.
|
||||
std::cerr << "Error: " << result.message() << std::endl;
|
||||
return 1;
|
||||
}
|
||||
// If cli.parse() succeeds, it also runs the commands and writes |exit_code|.
|
||||
|
||||
return exit_code;
|
||||
}
|
||||
69
cdc_stream/metrics_recorder.cc
Normal file
69
cdc_stream/metrics_recorder.cc
Normal file
@@ -0,0 +1,69 @@
|
||||
// Copyright 2022 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "cdc_stream/metrics_recorder.h"
|
||||
|
||||
#include "common/log.h"
|
||||
|
||||
namespace cdc_ft {
|
||||
|
||||
MetricsRecorder::MetricsRecorder(metrics::MetricsService* const metrics_service)
|
||||
: metrics_service_(metrics_service) {}
|
||||
|
||||
metrics::MetricsService* MetricsRecorder::GetMetricsService() const {
|
||||
return metrics_service_;
|
||||
}
|
||||
|
||||
MultiSessionMetricsRecorder::MultiSessionMetricsRecorder(
|
||||
metrics::MetricsService* const metrics_service)
|
||||
: MetricsRecorder(metrics_service),
|
||||
multisession_id_(Util::GenerateUniqueId()) {}
|
||||
|
||||
MultiSessionMetricsRecorder::~MultiSessionMetricsRecorder() = default;
|
||||
|
||||
void MultiSessionMetricsRecorder::RecordEvent(metrics::DeveloperLogEvent event,
|
||||
metrics::EventType code) const {
|
||||
if (!event.as_manager_data) {
|
||||
event.as_manager_data =
|
||||
std::make_unique<metrics::AssetStreamingManagerData>();
|
||||
}
|
||||
event.as_manager_data->multisession_id = multisession_id_;
|
||||
metrics_service_->RecordEvent(std::move(event), code);
|
||||
}
|
||||
|
||||
SessionMetricsRecorder::SessionMetricsRecorder(
|
||||
metrics::MetricsService* const metrics_service,
|
||||
const std::string& multisession_id, const std::string& project_id,
|
||||
const std::string& organization_id)
|
||||
: MetricsRecorder(metrics_service),
|
||||
multisession_id_(multisession_id),
|
||||
project_id_(project_id),
|
||||
organization_id_(organization_id),
|
||||
session_id_(Util::GenerateUniqueId()) {}
|
||||
|
||||
SessionMetricsRecorder::~SessionMetricsRecorder() = default;
|
||||
|
||||
void SessionMetricsRecorder::RecordEvent(metrics::DeveloperLogEvent event,
|
||||
metrics::EventType code) const {
|
||||
if (!event.as_manager_data) {
|
||||
event.as_manager_data =
|
||||
std::make_unique<metrics::AssetStreamingManagerData>();
|
||||
}
|
||||
event.as_manager_data->multisession_id = multisession_id_;
|
||||
event.as_manager_data->session_id = session_id_;
|
||||
event.project_id = project_id_;
|
||||
event.organization_id = organization_id_;
|
||||
metrics_service_->RecordEvent(std::move(event), code);
|
||||
}
|
||||
} // namespace cdc_ft
|
||||
77
cdc_stream/metrics_recorder.h
Normal file
77
cdc_stream/metrics_recorder.h
Normal 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.
|
||||
*/
|
||||
|
||||
#ifndef CDC_STREAM_METRICS_RECORDER_H_
|
||||
#define CDC_STREAM_METRICS_RECORDER_H_
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "common/util.h"
|
||||
#include "metrics/enums.h"
|
||||
#include "metrics/messages.h"
|
||||
#include "metrics/metrics.h"
|
||||
|
||||
namespace cdc_ft {
|
||||
|
||||
class MetricsRecorder {
|
||||
public:
|
||||
virtual void RecordEvent(metrics::DeveloperLogEvent event,
|
||||
metrics::EventType code) const = 0;
|
||||
|
||||
virtual metrics::MetricsService* GetMetricsService() const;
|
||||
|
||||
protected:
|
||||
explicit MetricsRecorder(metrics::MetricsService* const metrics_service);
|
||||
metrics::MetricsService* const metrics_service_;
|
||||
};
|
||||
|
||||
class MultiSessionMetricsRecorder : public MetricsRecorder {
|
||||
public:
|
||||
explicit MultiSessionMetricsRecorder(
|
||||
metrics::MetricsService* const metrics_service);
|
||||
~MultiSessionMetricsRecorder();
|
||||
|
||||
virtual void RecordEvent(metrics::DeveloperLogEvent event,
|
||||
metrics::EventType code) const;
|
||||
|
||||
const std::string& MultiSessionId() const { return multisession_id_; }
|
||||
|
||||
private:
|
||||
std::string multisession_id_;
|
||||
};
|
||||
|
||||
class SessionMetricsRecorder : public MetricsRecorder {
|
||||
public:
|
||||
explicit SessionMetricsRecorder(
|
||||
metrics::MetricsService* const metrics_service,
|
||||
const std::string& multisession_id, const std::string& project_id,
|
||||
const std::string& organization_id);
|
||||
~SessionMetricsRecorder();
|
||||
|
||||
virtual void RecordEvent(metrics::DeveloperLogEvent event,
|
||||
metrics::EventType code) const;
|
||||
|
||||
const std::string& SessionId() const { return session_id_; }
|
||||
|
||||
private:
|
||||
std::string multisession_id_;
|
||||
std::string session_id_;
|
||||
std::string project_id_;
|
||||
std::string organization_id_;
|
||||
};
|
||||
|
||||
} // namespace cdc_ft
|
||||
|
||||
#endif // CDC_STREAM_METRICS_RECORDER_H_
|
||||
131
cdc_stream/metrics_recorder_test.cc
Normal file
131
cdc_stream/metrics_recorder_test.cc
Normal file
@@ -0,0 +1,131 @@
|
||||
// Copyright 2022 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "cdc_stream/metrics_recorder.h"
|
||||
|
||||
#include "common/status_test_macros.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "metrics/metrics.h"
|
||||
|
||||
namespace cdc_ft {
|
||||
namespace {
|
||||
|
||||
struct MetricsRecord {
|
||||
MetricsRecord(metrics::DeveloperLogEvent dev_log_event,
|
||||
metrics::EventType code)
|
||||
: dev_log_event(std::move(dev_log_event)), code(code) {}
|
||||
metrics::DeveloperLogEvent dev_log_event;
|
||||
metrics::EventType code;
|
||||
};
|
||||
|
||||
class MetricsServiceForTesting : public metrics::MetricsService {
|
||||
public:
|
||||
MetricsServiceForTesting() {
|
||||
metrics_records_ = new std::vector<MetricsRecord>();
|
||||
}
|
||||
|
||||
~MetricsServiceForTesting() { delete metrics_records_; }
|
||||
|
||||
void RecordEvent(metrics::DeveloperLogEvent event,
|
||||
metrics::EventType code) const override {
|
||||
metrics_records_->push_back(MetricsRecord(std::move(event), code));
|
||||
}
|
||||
|
||||
int NumberOfRecordRequests() { return (int)metrics_records_->size(); }
|
||||
|
||||
std::vector<MetricsRecord> GetEventsAndClear() {
|
||||
return std::move(*metrics_records_);
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<MetricsRecord>* metrics_records_;
|
||||
};
|
||||
|
||||
class MetricsRecorderTest : public ::testing::Test {
|
||||
public:
|
||||
void SetUp() override { metrics_service_ = new MetricsServiceForTesting(); }
|
||||
|
||||
void TearDown() override { delete metrics_service_; }
|
||||
|
||||
protected:
|
||||
MetricsServiceForTesting* metrics_service_;
|
||||
};
|
||||
|
||||
TEST_F(MetricsRecorderTest, SendEventWithMultisessionId) {
|
||||
MultiSessionMetricsRecorder target(metrics_service_);
|
||||
metrics::DeveloperLogEvent q_evt;
|
||||
q_evt.project_id = "proj/id";
|
||||
q_evt.organization_id = "org/id";
|
||||
|
||||
target.RecordEvent(std::move(q_evt), metrics::EventType::kMultiSessionStart);
|
||||
EXPECT_EQ(metrics_service_->NumberOfRecordRequests(), 1);
|
||||
std::vector<MetricsRecord> requests = metrics_service_->GetEventsAndClear();
|
||||
EXPECT_EQ(requests[0].code, metrics::EventType::kMultiSessionStart);
|
||||
metrics::DeveloperLogEvent expected_evt;
|
||||
expected_evt.project_id = "proj/id";
|
||||
expected_evt.organization_id = "org/id";
|
||||
expected_evt.as_manager_data =
|
||||
std::make_unique<metrics::AssetStreamingManagerData>();
|
||||
expected_evt.as_manager_data->multisession_id = target.MultiSessionId();
|
||||
EXPECT_EQ(requests[0].dev_log_event, expected_evt);
|
||||
EXPECT_FALSE(target.MultiSessionId().empty());
|
||||
|
||||
q_evt = metrics::DeveloperLogEvent();
|
||||
q_evt.project_id = "proj/id";
|
||||
q_evt.organization_id = "org/id";
|
||||
target.RecordEvent(std::move(q_evt), metrics::EventType::kMultiSessionStart);
|
||||
EXPECT_EQ(metrics_service_->NumberOfRecordRequests(), 1);
|
||||
std::vector<MetricsRecord> requests2 = metrics_service_->GetEventsAndClear();
|
||||
EXPECT_EQ(requests2[0].code, metrics::EventType::kMultiSessionStart);
|
||||
EXPECT_EQ(requests2[0].dev_log_event, requests[0].dev_log_event);
|
||||
|
||||
MultiSessionMetricsRecorder target2(metrics_service_);
|
||||
EXPECT_NE(target2.MultiSessionId(), target.MultiSessionId());
|
||||
}
|
||||
|
||||
TEST_F(MetricsRecorderTest, SendEventWithSessionId) {
|
||||
SessionMetricsRecorder target(metrics_service_, "id1", "m_proj", "m_org");
|
||||
metrics::DeveloperLogEvent q_evt;
|
||||
q_evt.project_id = "proj/id";
|
||||
q_evt.organization_id = "org/id";
|
||||
|
||||
target.RecordEvent(std::move(q_evt), metrics::EventType::kSessionStart);
|
||||
EXPECT_EQ(metrics_service_->NumberOfRecordRequests(), 1);
|
||||
std::vector<MetricsRecord> requests = metrics_service_->GetEventsAndClear();
|
||||
EXPECT_EQ(requests[0].code, metrics::EventType::kSessionStart);
|
||||
metrics::DeveloperLogEvent expected_evt;
|
||||
expected_evt.project_id = "m_proj";
|
||||
expected_evt.organization_id = "m_org";
|
||||
expected_evt.as_manager_data =
|
||||
std::make_unique<metrics::AssetStreamingManagerData>();
|
||||
expected_evt.as_manager_data->multisession_id = "id1";
|
||||
expected_evt.as_manager_data->session_id = target.SessionId();
|
||||
EXPECT_EQ(requests[0].dev_log_event, expected_evt);
|
||||
EXPECT_FALSE(target.SessionId().empty());
|
||||
|
||||
q_evt = metrics::DeveloperLogEvent();
|
||||
q_evt.project_id = "proj/id";
|
||||
q_evt.organization_id = "org/id";
|
||||
target.RecordEvent(std::move(q_evt), metrics::EventType::kSessionStart);
|
||||
EXPECT_EQ(metrics_service_->NumberOfRecordRequests(), 1);
|
||||
std::vector<MetricsRecord> requests2 = metrics_service_->GetEventsAndClear();
|
||||
EXPECT_EQ(requests2[0].code, metrics::EventType::kSessionStart);
|
||||
EXPECT_EQ(requests2[0].dev_log_event, requests[0].dev_log_event);
|
||||
|
||||
SessionMetricsRecorder target2(metrics_service_, "id2", "m_proj", "m_org");
|
||||
EXPECT_NE(target2.SessionId(), target.SessionId());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace cdc_ft
|
||||
698
cdc_stream/multi_session.cc
Normal file
698
cdc_stream/multi_session.cc
Normal file
@@ -0,0 +1,698 @@
|
||||
// Copyright 2022 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "cdc_stream/multi_session.h"
|
||||
|
||||
#include "cdc_stream/session.h"
|
||||
#include "common/file_watcher_win.h"
|
||||
#include "common/log.h"
|
||||
#include "common/path.h"
|
||||
#include "common/platform.h"
|
||||
#include "common/port_manager.h"
|
||||
#include "common/process.h"
|
||||
#include "common/util.h"
|
||||
#include "data_store/disk_data_store.h"
|
||||
#include "manifest/content_id.h"
|
||||
#include "manifest/manifest_iterator.h"
|
||||
#include "manifest/manifest_printer.h"
|
||||
#include "manifest/manifest_proto_defs.h"
|
||||
#include "metrics/enums.h"
|
||||
#include "metrics/messages.h"
|
||||
|
||||
namespace cdc_ft {
|
||||
namespace {
|
||||
|
||||
// Ports used by the asset streaming service for local port forwarding on
|
||||
// workstation and gamelet.
|
||||
constexpr int kAssetStreamPortFirst = 44433;
|
||||
constexpr int kAssetStreamPortLast = 44442;
|
||||
|
||||
// Stats output period (if enabled).
|
||||
constexpr double kStatsPrintDelaySec = 0.1f;
|
||||
|
||||
ManifestUpdater::Operator FileWatcherActionToOperation(
|
||||
FileWatcherWin::FileAction action) {
|
||||
switch (action) {
|
||||
case FileWatcherWin::FileAction::kAdded:
|
||||
return ManifestUpdater::Operator::kAdd;
|
||||
case FileWatcherWin::FileAction::kModified:
|
||||
return ManifestUpdater::Operator::kUpdate;
|
||||
case FileWatcherWin::FileAction::kDeleted:
|
||||
return ManifestUpdater::Operator::kDelete;
|
||||
}
|
||||
// The switch must cover all actions.
|
||||
LOG_ERROR("Unhandled action: %d", static_cast<int>(action));
|
||||
assert(false);
|
||||
return ManifestUpdater::Operator::kAdd;
|
||||
}
|
||||
|
||||
// Converts |modified_files| (as returned from the file watcher) into an
|
||||
// OperationList (as required by the manifest updater).
|
||||
ManifestUpdater::OperationList GetFileOperations(
|
||||
const FileWatcherWin::FileMap& modified_files) {
|
||||
AssetInfo ai;
|
||||
ManifestUpdater::OperationList ops;
|
||||
ops.reserve(modified_files.size());
|
||||
for (const auto& [path, info] : modified_files) {
|
||||
ai.path = path;
|
||||
ai.type = info.is_dir ? AssetProto::DIRECTORY : AssetProto::FILE;
|
||||
ai.size = info.size;
|
||||
ai.mtime = info.mtime;
|
||||
ops.emplace_back(FileWatcherActionToOperation(info.action), std::move(ai));
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
MultiSessionRunner::MultiSessionRunner(
|
||||
std::string src_dir, DataStoreWriter* data_store,
|
||||
ProcessFactory* process_factory, bool enable_stats,
|
||||
absl::Duration wait_duration, uint32_t num_updater_threads,
|
||||
MultiSessionMetricsRecorder const* metrics_recorder,
|
||||
ManifestUpdatedCb manifest_updated_cb)
|
||||
: src_dir_(std::move(src_dir)),
|
||||
data_store_(data_store),
|
||||
process_factory_(process_factory),
|
||||
file_chunks_(enable_stats),
|
||||
wait_duration_(wait_duration),
|
||||
num_updater_threads_(num_updater_threads),
|
||||
manifest_updated_cb_(std::move(manifest_updated_cb)),
|
||||
metrics_recorder_(metrics_recorder) {
|
||||
assert(metrics_recorder_);
|
||||
}
|
||||
|
||||
absl::Status MultiSessionRunner::Initialize(int port,
|
||||
AssetStreamServerType type,
|
||||
ContentSentHandler content_sent) {
|
||||
// Create the manifest updater.
|
||||
UpdaterConfig cfg;
|
||||
cfg.num_threads = num_updater_threads_;
|
||||
cfg.src_dir = src_dir_;
|
||||
|
||||
assert(!manifest_updater_);
|
||||
manifest_updater_ =
|
||||
std::make_unique<ManifestUpdater>(data_store_, std::move(cfg));
|
||||
|
||||
// Let the manifest updater handle requests to prioritize certain assets.
|
||||
PrioritizeAssetsHandler prio_assets =
|
||||
std::bind(&ManifestUpdater::AddPriorityAssets, manifest_updater_.get(),
|
||||
std::placeholders::_1);
|
||||
|
||||
// Start the server.
|
||||
assert(!server_);
|
||||
server_ = AssetStreamServer::Create(type, src_dir_, data_store_,
|
||||
&file_chunks_, std::move(content_sent),
|
||||
std::move(prio_assets));
|
||||
assert(server_);
|
||||
RETURN_IF_ERROR(server_->Start(port),
|
||||
"Failed to start asset stream server for '%s'", src_dir_);
|
||||
|
||||
assert(!thread_);
|
||||
thread_ = std::make_unique<std::thread>([this]() { Run(); });
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status MultiSessionRunner::Shutdown() {
|
||||
// Send shutdown signal.
|
||||
{
|
||||
absl::MutexLock lock(&mutex_);
|
||||
shutdown_ = true;
|
||||
}
|
||||
if (thread_) {
|
||||
if (thread_->joinable()) thread_->join();
|
||||
thread_.reset();
|
||||
}
|
||||
|
||||
// Shut down asset stream server.
|
||||
if (server_) {
|
||||
server_->Shutdown();
|
||||
server_.reset();
|
||||
}
|
||||
|
||||
return status_;
|
||||
}
|
||||
|
||||
absl::Status MultiSessionRunner::WaitForManifestAck(
|
||||
const std::string& instance_id, absl::Duration fuse_timeout) {
|
||||
{
|
||||
absl::MutexLock lock(&mutex_);
|
||||
|
||||
LOG_INFO("Waiting for manifest to be available");
|
||||
auto cond = [this]() { return manifest_set_ || !status_.ok(); };
|
||||
mutex_.Await(absl::Condition(&cond));
|
||||
|
||||
if (!status_.ok())
|
||||
return WrapStatus(status_, "Failed to set up streaming session for '%s'",
|
||||
src_dir_);
|
||||
}
|
||||
|
||||
LOG_INFO("Waiting for FUSE ack");
|
||||
assert(server_);
|
||||
RETURN_IF_ERROR(server_->WaitForManifestAck(instance_id, fuse_timeout));
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status MultiSessionRunner::Status() {
|
||||
absl::MutexLock lock(&mutex_);
|
||||
return status_;
|
||||
}
|
||||
|
||||
ContentIdProto MultiSessionRunner::ManifestId() const {
|
||||
assert(server_);
|
||||
return server_->GetManifestId();
|
||||
}
|
||||
|
||||
void MultiSessionRunner::Run() {
|
||||
// Set up file watcher.
|
||||
// The streamed path should be a directory and exist at the beginning.
|
||||
FileWatcherWin watcher(src_dir_);
|
||||
absl::Status status = watcher.StartWatching([this]() { OnFilesChanged(); },
|
||||
[this]() { OnDirRecreated(); });
|
||||
if (!status.ok()) {
|
||||
SetStatus(
|
||||
WrapStatus(status, "Failed to update manifest for '%s'", src_dir_));
|
||||
return;
|
||||
}
|
||||
|
||||
// Push the intermediate manifest(s) and the final version with this handler.
|
||||
auto push_handler = [this](const ContentIdProto& manifest_id) {
|
||||
SetManifest(manifest_id);
|
||||
};
|
||||
|
||||
// Bring the manifest up to date.
|
||||
LOG_INFO("Updating manifest for '%s'...", src_dir_);
|
||||
Stopwatch sw;
|
||||
status = manifest_updater_->UpdateAll(&file_chunks_, push_handler);
|
||||
RecordManifestUpdate(*manifest_updater_, sw.Elapsed(),
|
||||
metrics::UpdateTrigger::kInitUpdateAll, status);
|
||||
if (!status.ok()) {
|
||||
SetStatus(
|
||||
WrapStatus(status, "Failed to update manifest for '%s'", src_dir_));
|
||||
return;
|
||||
}
|
||||
RecordMultiSessionStart(*manifest_updater_);
|
||||
LOG_INFO("Manifest for '%s' updated in %0.3f seconds", src_dir_,
|
||||
sw.ElapsedSeconds());
|
||||
|
||||
while (!shutdown_) {
|
||||
FileWatcherWin::FileMap modified_files;
|
||||
bool clean_manifest = false;
|
||||
{
|
||||
// Wait for changes.
|
||||
absl::MutexLock lock(&mutex_);
|
||||
|
||||
bool prev_files_changed = files_changed_;
|
||||
absl::Duration timeout =
|
||||
absl::Seconds(file_chunks_.HasStats() ? kStatsPrintDelaySec : 3600.0);
|
||||
if (files_changed_) {
|
||||
timeout = std::max(wait_duration_ - files_changed_timer_.Elapsed(),
|
||||
absl::Milliseconds(1));
|
||||
} else {
|
||||
files_changed_timer_.Reset();
|
||||
}
|
||||
auto cond = [this]() {
|
||||
return shutdown_ || files_changed_ || dir_recreated_;
|
||||
};
|
||||
mutex_.AwaitWithTimeout(absl::Condition(&cond), timeout);
|
||||
|
||||
// If |files_changed_| became true, wait some more time before updating
|
||||
// the manifest.
|
||||
if (!prev_files_changed && files_changed_) files_changed_timer_.Reset();
|
||||
|
||||
// Shut down.
|
||||
if (shutdown_) {
|
||||
LOG_INFO("MultiSession('%s'): Shutting down", src_dir_);
|
||||
break;
|
||||
}
|
||||
|
||||
// Pick up modified files.
|
||||
if (!dir_recreated_ && files_changed_ &&
|
||||
files_changed_timer_.Elapsed() > wait_duration_) {
|
||||
modified_files = watcher.GetModifiedFiles();
|
||||
files_changed_ = false;
|
||||
files_changed_timer_.Reset();
|
||||
}
|
||||
|
||||
if (dir_recreated_) {
|
||||
clean_manifest = true;
|
||||
dir_recreated_ = false;
|
||||
}
|
||||
} // mutex_ lock
|
||||
|
||||
if (clean_manifest) {
|
||||
LOG_DEBUG(
|
||||
"Streamed directory '%s' was possibly re-created or not all changes "
|
||||
"were detected, re-building the manifest",
|
||||
src_dir_);
|
||||
modified_files.clear();
|
||||
sw.Reset();
|
||||
status = manifest_updater_->UpdateAll(&file_chunks_, push_handler);
|
||||
RecordManifestUpdate(*manifest_updater_, sw.Elapsed(),
|
||||
metrics::UpdateTrigger::kRunningUpdateAll, status);
|
||||
if (!status.ok()) {
|
||||
LOG_WARNING(
|
||||
"Updating manifest for '%s' after re-creating directory failed: "
|
||||
"'%s'",
|
||||
src_dir_, status.ToString());
|
||||
SetManifest(manifest_updater_->DefaultManifestId());
|
||||
}
|
||||
} else if (!modified_files.empty()) {
|
||||
ManifestUpdater::OperationList ops = GetFileOperations(modified_files);
|
||||
sw.Reset();
|
||||
status = manifest_updater_->Update(&ops, &file_chunks_, push_handler);
|
||||
RecordManifestUpdate(*manifest_updater_, sw.Elapsed(),
|
||||
metrics::UpdateTrigger::kRegularUpdate, status);
|
||||
if (!status.ok()) {
|
||||
LOG_WARNING("Updating manifest for '%s' failed: %s", src_dir_,
|
||||
status.ToString());
|
||||
SetManifest(manifest_updater_->DefaultManifestId());
|
||||
}
|
||||
}
|
||||
|
||||
// Update stats output.
|
||||
file_chunks_.PrintStats();
|
||||
}
|
||||
}
|
||||
|
||||
void MultiSessionRunner::RecordManifestUpdate(
|
||||
const ManifestUpdater& manifest_updater, absl::Duration duration,
|
||||
metrics::UpdateTrigger trigger, absl::Status status) {
|
||||
metrics::DeveloperLogEvent evt;
|
||||
evt.as_manager_data = std::make_unique<metrics::AssetStreamingManagerData>();
|
||||
evt.as_manager_data->manifest_update_data =
|
||||
std::make_unique<metrics::ManifestUpdateData>();
|
||||
evt.as_manager_data->manifest_update_data->local_duration_ms =
|
||||
absl::ToInt64Milliseconds(duration);
|
||||
evt.as_manager_data->manifest_update_data->status = status.code();
|
||||
evt.as_manager_data->manifest_update_data->trigger = trigger;
|
||||
const UpdaterStats& stats = manifest_updater.Stats();
|
||||
evt.as_manager_data->manifest_update_data->total_assets_added_or_updated =
|
||||
stats.total_assets_added_or_updated;
|
||||
evt.as_manager_data->manifest_update_data->total_assets_deleted =
|
||||
stats.total_assets_deleted;
|
||||
evt.as_manager_data->manifest_update_data->total_chunks = stats.total_chunks;
|
||||
evt.as_manager_data->manifest_update_data->total_files_added_or_updated =
|
||||
stats.total_files_added_or_updated;
|
||||
evt.as_manager_data->manifest_update_data->total_files_failed =
|
||||
stats.total_files_failed;
|
||||
evt.as_manager_data->manifest_update_data->total_processed_bytes =
|
||||
stats.total_processed_bytes;
|
||||
metrics_recorder_->RecordEvent(std::move(evt),
|
||||
metrics::EventType::kManifestUpdated);
|
||||
}
|
||||
|
||||
void MultiSessionRunner::RecordMultiSessionStart(
|
||||
const ManifestUpdater& manifest_updater) {
|
||||
metrics::DeveloperLogEvent evt;
|
||||
evt.as_manager_data = std::make_unique<metrics::AssetStreamingManagerData>();
|
||||
evt.as_manager_data->multi_session_start_data =
|
||||
std::make_unique<metrics::MultiSessionStartData>();
|
||||
ManifestIterator manifest_iter(data_store_);
|
||||
absl::Status status = manifest_iter.Open(manifest_updater.ManifestId());
|
||||
if (status.ok()) {
|
||||
const AssetProto* entry = &manifest_iter.Manifest().root_dir();
|
||||
uint32_t file_count = 0;
|
||||
uint64_t total_chunks = 0;
|
||||
uint64_t total_processed_bytes = 0;
|
||||
do {
|
||||
if (entry->type() == AssetProto::FILE) {
|
||||
++file_count;
|
||||
total_chunks += entry->file_chunks_size();
|
||||
total_processed_bytes += entry->file_size();
|
||||
for (const IndirectChunkListProto& icl :
|
||||
entry->file_indirect_chunks()) {
|
||||
ChunkListProto list;
|
||||
status = data_store_->GetProto(icl.chunk_list_id(), &list);
|
||||
if (status.ok()) {
|
||||
total_chunks += list.chunks_size();
|
||||
} else {
|
||||
LOG_WARNING("Could not get proto by id: '%s'. %s",
|
||||
ContentId::ToHexString(icl.chunk_list_id()),
|
||||
status.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
} while ((entry = manifest_iter.NextEntry()) != nullptr);
|
||||
evt.as_manager_data->multi_session_start_data->file_count = file_count;
|
||||
evt.as_manager_data->multi_session_start_data->chunk_count = total_chunks;
|
||||
evt.as_manager_data->multi_session_start_data->byte_count =
|
||||
total_processed_bytes;
|
||||
} else {
|
||||
LOG_WARNING("Could not open manifest by id: '%s'. %s",
|
||||
ContentId::ToHexString(manifest_updater.ManifestId()),
|
||||
status.ToString());
|
||||
}
|
||||
evt.as_manager_data->multi_session_start_data->min_chunk_size =
|
||||
static_cast<uint64_t>(manifest_updater.Config().min_chunk_size);
|
||||
evt.as_manager_data->multi_session_start_data->avg_chunk_size =
|
||||
static_cast<uint64_t>(manifest_updater.Config().avg_chunk_size);
|
||||
evt.as_manager_data->multi_session_start_data->max_chunk_size =
|
||||
static_cast<uint64_t>(manifest_updater.Config().max_chunk_size);
|
||||
metrics_recorder_->RecordEvent(std::move(evt),
|
||||
metrics::EventType::kMultiSessionStart);
|
||||
}
|
||||
|
||||
void MultiSessionRunner::SetStatus(absl::Status status)
|
||||
ABSL_LOCKS_EXCLUDED(mutex_) {
|
||||
absl::MutexLock lock(&mutex_);
|
||||
status_ = std::move(status);
|
||||
}
|
||||
|
||||
void MultiSessionRunner::OnFilesChanged() {
|
||||
absl::MutexLock lock(&mutex_);
|
||||
files_changed_ = true;
|
||||
}
|
||||
|
||||
void MultiSessionRunner::OnDirRecreated() {
|
||||
absl::MutexLock lock(&mutex_);
|
||||
dir_recreated_ = true;
|
||||
}
|
||||
|
||||
void MultiSessionRunner::SetManifest(const ContentIdProto& manifest_id) {
|
||||
server_->SetManifestId(manifest_id);
|
||||
if (Log::Instance()->GetLogLevel() <= LogLevel::kVerbose) {
|
||||
ManifestPrinter printer;
|
||||
ManifestProto manifest_proto;
|
||||
absl::Status status = data_store_->GetProto(manifest_id, &manifest_proto);
|
||||
std::string manifest_text;
|
||||
printer.PrintToString(manifest_proto, &manifest_text);
|
||||
if (status.ok()) {
|
||||
LOG_DEBUG("Set manifest '%s'\n'%s'", ContentId::ToHexString(manifest_id),
|
||||
manifest_text);
|
||||
} else {
|
||||
LOG_WARNING("Could not retrieve manifest from the data store '%s'",
|
||||
ContentId::ToHexString(manifest_id));
|
||||
}
|
||||
}
|
||||
|
||||
// Notify thread that starts the streaming session that a manifest has been
|
||||
// set.
|
||||
absl::MutexLock lock(&mutex_);
|
||||
manifest_set_ = true;
|
||||
if (manifest_updated_cb_) {
|
||||
manifest_updated_cb_();
|
||||
}
|
||||
}
|
||||
|
||||
MultiSession::MultiSession(std::string src_dir, SessionConfig cfg,
|
||||
ProcessFactory* process_factory,
|
||||
MultiSessionMetricsRecorder const* metrics_recorder,
|
||||
std::unique_ptr<DataStoreWriter> data_store)
|
||||
: src_dir_(src_dir),
|
||||
cfg_(cfg),
|
||||
process_factory_(process_factory),
|
||||
data_store_(std::move(data_store)),
|
||||
metrics_recorder_(metrics_recorder) {
|
||||
assert(metrics_recorder_);
|
||||
}
|
||||
|
||||
MultiSession::~MultiSession() {
|
||||
absl::Status status = Shutdown();
|
||||
if (!status.ok()) {
|
||||
LOG_ERROR("Shutdown streaming from '%s' failed: %s", src_dir_,
|
||||
status.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
absl::Status MultiSession::Initialize() {
|
||||
// |data_store_| is not set in production, but it can be overridden for tests.
|
||||
if (!data_store_) {
|
||||
std::string cache_path;
|
||||
ASSIGN_OR_RETURN(cache_path, GetCachePath(src_dir_));
|
||||
ASSIGN_OR_RETURN(data_store_,
|
||||
DiskDataStore::Create(/*depth=*/0, cache_path,
|
||||
/*create_dirs=*/true),
|
||||
"Failed to create data store for '%s'", cache_path);
|
||||
}
|
||||
|
||||
// Find an available local port.
|
||||
std::unordered_set<int> ports;
|
||||
ASSIGN_OR_RETURN(
|
||||
ports,
|
||||
PortManager::FindAvailableLocalPorts(kAssetStreamPortFirst,
|
||||
kAssetStreamPortLast, "127.0.0.1",
|
||||
process_factory_),
|
||||
"Failed to find an available local port in the range [%d, %d]",
|
||||
kAssetStreamPortFirst, kAssetStreamPortLast);
|
||||
assert(!ports.empty());
|
||||
local_asset_stream_port_ = *ports.begin();
|
||||
|
||||
assert(!runner_);
|
||||
runner_ = std::make_unique<MultiSessionRunner>(
|
||||
src_dir_, data_store_.get(), process_factory_, cfg_.stats,
|
||||
absl::Milliseconds(cfg_.file_change_wait_duration_ms),
|
||||
cfg_.manifest_updater_threads, metrics_recorder_);
|
||||
RETURN_IF_ERROR(runner_->Initialize(
|
||||
local_asset_stream_port_, AssetStreamServerType::kGrpc,
|
||||
[this](uint64_t bc, uint64_t cc, std::string id) {
|
||||
this->OnContentSent(bc, cc, id);
|
||||
}),
|
||||
"Failed to initialize session runner");
|
||||
StartHeartBeatCheck();
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status MultiSession::Shutdown() {
|
||||
// Stop all sessions.
|
||||
// TODO: Record error on multi-session end.
|
||||
metrics_recorder_->RecordEvent(metrics::DeveloperLogEvent(),
|
||||
metrics::EventType::kMultiSessionEnd);
|
||||
{
|
||||
absl::WriterMutexLock lock(&shutdownMu_);
|
||||
shutdown_ = true;
|
||||
}
|
||||
while (!sessions_.empty()) {
|
||||
std::string instance_id = sessions_.begin()->first;
|
||||
RETURN_IF_ERROR(StopSession(instance_id),
|
||||
"Failed to stop session for instance id '%s'", instance_id);
|
||||
sessions_.erase(instance_id);
|
||||
}
|
||||
|
||||
absl::Status status;
|
||||
if (runner_) {
|
||||
status = runner_->Shutdown();
|
||||
}
|
||||
|
||||
if (heartbeat_watcher_.joinable()) {
|
||||
heartbeat_watcher_.join();
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
absl::Status MultiSession::Status() {
|
||||
return runner_ ? runner_->Status() : absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status MultiSession::StartSession(const std::string& instance_id,
|
||||
const SessionTarget& target,
|
||||
const std::string& project_id,
|
||||
const std::string& organization_id) {
|
||||
absl::MutexLock lock(&sessions_mutex_);
|
||||
|
||||
if (sessions_.find(instance_id) != sessions_.end()) {
|
||||
return absl::InvalidArgumentError(absl::StrFormat(
|
||||
"Session for instance id '%s' already exists", instance_id));
|
||||
}
|
||||
|
||||
if (!runner_)
|
||||
return absl::FailedPreconditionError("MultiSession not started");
|
||||
|
||||
absl::Status runner_status = runner_->Status();
|
||||
if (!runner_status.ok()) {
|
||||
return WrapStatus(runner_status,
|
||||
"Failed to set up streaming session for '%s'", src_dir_);
|
||||
}
|
||||
|
||||
auto metrics_recorder = std::make_unique<SessionMetricsRecorder>(
|
||||
metrics_recorder_->GetMetricsService(),
|
||||
metrics_recorder_->MultiSessionId(), project_id, organization_id);
|
||||
|
||||
auto session = std::make_unique<Session>(
|
||||
instance_id, target, cfg_, process_factory_, std::move(metrics_recorder));
|
||||
RETURN_IF_ERROR(session->Start(local_asset_stream_port_,
|
||||
kAssetStreamPortFirst, kAssetStreamPortLast));
|
||||
|
||||
// Wait for the FUSE to receive the first intermediate manifest.
|
||||
RETURN_IF_ERROR(runner_->WaitForManifestAck(instance_id, absl::Seconds(5)));
|
||||
|
||||
sessions_[instance_id] = std::move(session);
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status MultiSession::StopSession(const std::string& instance_id) {
|
||||
absl::MutexLock lock(&sessions_mutex_);
|
||||
|
||||
if (sessions_.find(instance_id) == sessions_.end()) {
|
||||
return absl::NotFoundError(
|
||||
absl::StrFormat("No session for instance id '%s' found", instance_id));
|
||||
}
|
||||
|
||||
if (!runner_)
|
||||
return absl::FailedPreconditionError("MultiSession not started");
|
||||
|
||||
RETURN_IF_ERROR(sessions_[instance_id]->Stop());
|
||||
sessions_.erase(instance_id);
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
bool MultiSession::HasSession(const std::string& instance_id) {
|
||||
absl::ReaderMutexLock lock(&sessions_mutex_);
|
||||
return sessions_.find(instance_id) != sessions_.end();
|
||||
}
|
||||
|
||||
bool MultiSession::IsSessionHealthy(const std::string& instance_id) {
|
||||
absl::ReaderMutexLock lock(&sessions_mutex_);
|
||||
auto iter = sessions_.find(instance_id);
|
||||
if (iter == sessions_.end()) return false;
|
||||
Session* session = iter->second.get();
|
||||
assert(session);
|
||||
return session->IsHealthy();
|
||||
}
|
||||
|
||||
bool MultiSession::Empty() {
|
||||
absl::ReaderMutexLock lock(&sessions_mutex_);
|
||||
return sessions_.empty();
|
||||
}
|
||||
|
||||
uint32_t MultiSession::GetSessionCount() {
|
||||
absl::ReaderMutexLock lock(&sessions_mutex_);
|
||||
return static_cast<uint32_t>(sessions_.size());
|
||||
}
|
||||
|
||||
// static
|
||||
std::string MultiSession::GetCacheDir(std::string dir) {
|
||||
// Get full path, or else "..\foo" and "C:\foo" are treated differently, even
|
||||
// if they map to the same directory.
|
||||
dir = path::GetFullPath(dir);
|
||||
#if PLATFORM_WINDOWS
|
||||
// On Windows, casing is ignored.
|
||||
std::for_each(dir.begin(), dir.end(), [](char& c) { c = tolower(c); });
|
||||
#endif
|
||||
path::EnsureEndsWithPathSeparator(&dir);
|
||||
dir = path::ToUnix(std::move(dir));
|
||||
ContentIdProto id = ContentId::FromDataString(dir);
|
||||
|
||||
// Replace invalid characters by _.
|
||||
std::for_each(dir.begin(), dir.end(), [](char& c) {
|
||||
static constexpr char invalid_chars[] = "<>:\"/\\|?*";
|
||||
if (strchr(invalid_chars, c)) c = '_';
|
||||
});
|
||||
|
||||
return dir + ContentId::ToHexString(id).substr(0, kDirHashLen);
|
||||
}
|
||||
|
||||
// static
|
||||
absl::StatusOr<std::string> MultiSession::GetCachePath(
|
||||
const std::string& src_dir, size_t max_len) {
|
||||
std::string appdata_path;
|
||||
#if PLATFORM_WINDOWS
|
||||
RETURN_IF_ERROR(
|
||||
path::GetKnownFolderPath(path::FolderId::kRoamingAppData, &appdata_path),
|
||||
"Failed to get roaming appdata path");
|
||||
#elif PLATFORM_LINUX
|
||||
RETURN_IF_ERROR(path::GetEnv("HOME", &appdata_path));
|
||||
path::Append(&appdata_path, ".cache");
|
||||
#endif
|
||||
|
||||
std::string base_dir = path::Join(appdata_path, "GGP", "asset_streaming");
|
||||
std::string cache_dir = GetCacheDir(src_dir);
|
||||
|
||||
size_t total_size = base_dir.size() + 1 + cache_dir.size();
|
||||
if (total_size <= max_len) return path::Join(base_dir, cache_dir);
|
||||
|
||||
// Path needs to be shortened. Remove |to_remove| many chars from the
|
||||
// beginning of |cache_dir|, but keep the hash (last kDirHashLen bytes).
|
||||
size_t to_remove = total_size - max_len;
|
||||
assert(cache_dir.size() >= kDirHashLen);
|
||||
if (to_remove > cache_dir.size() - kDirHashLen)
|
||||
to_remove = cache_dir.size() - kDirHashLen;
|
||||
|
||||
// Remove UTF8 code points from the beginning.
|
||||
size_t start = 0;
|
||||
while (start < to_remove) {
|
||||
int codepoint_len = Util::Utf8CodePointLen(cache_dir.data() + start);
|
||||
// For invalid code points (codepoint_len == 0), just eat byte by byte.
|
||||
start += std::max(codepoint_len, 1);
|
||||
}
|
||||
|
||||
assert(start + kDirHashLen <= cache_dir.size());
|
||||
return path::Join(base_dir, cache_dir.substr(start));
|
||||
}
|
||||
|
||||
void MultiSession::RecordMultiSessionEvent(metrics::DeveloperLogEvent event,
|
||||
metrics::EventType code) {
|
||||
metrics_recorder_->RecordEvent(std::move(event), code);
|
||||
}
|
||||
|
||||
void MultiSession::RecordSessionEvent(metrics::DeveloperLogEvent event,
|
||||
metrics::EventType code,
|
||||
const std::string& instance_id) {
|
||||
Session* session = FindSession(instance_id);
|
||||
if (session) {
|
||||
session->RecordEvent(std::move(event), code);
|
||||
}
|
||||
}
|
||||
|
||||
Session* MultiSession::FindSession(const std::string& instance_id) {
|
||||
absl::ReaderMutexLock lock(&sessions_mutex_);
|
||||
auto session_it = sessions_.find(instance_id);
|
||||
if (session_it == sessions_.end()) {
|
||||
return nullptr;
|
||||
}
|
||||
return session_it->second.get();
|
||||
}
|
||||
|
||||
void MultiSession::OnContentSent(size_t byte_count, size_t chunck_count,
|
||||
std::string instance_id) {
|
||||
if (instance_id.empty()) {
|
||||
// |instance_id| is empty only in case when manifest wasn't acknowledged by
|
||||
// the instance yet (ConfigStreamServiceImpl::AckManifestIdReceived was not
|
||||
// invoked). This means MultiSession::StartSession is still waiting for
|
||||
// manifest acknowledge and |sessions_mutex_| is currently locked. In this
|
||||
// case invoking MultiSession::FindSession and waiting for |sessions_mutex_|
|
||||
// to get unlocked will block the current thread, which is also responsible
|
||||
// for receiving a call at ConfigStreamServiceImpl::AckManifestIdReceived.
|
||||
// This causes a deadlock and leads to a DeadlineExceeded error.
|
||||
LOG_WARNING("Cannot record session content for an empty instance_id.");
|
||||
return;
|
||||
}
|
||||
Session* session = FindSession(instance_id);
|
||||
if (session == nullptr) {
|
||||
LOG_WARNING("Failed to find active session by instance id '%s'",
|
||||
instance_id);
|
||||
return;
|
||||
}
|
||||
session->OnContentSent(byte_count, chunck_count);
|
||||
}
|
||||
|
||||
void MultiSession::StartHeartBeatCheck() {
|
||||
heartbeat_watcher_ = std::thread([this]() ABSL_LOCKS_EXCLUDED(shutdownMu_) {
|
||||
auto cond = [this]() { return shutdown_; };
|
||||
while (!shutdownMu_.LockWhenWithTimeout(absl::Condition(&cond),
|
||||
absl::Minutes(5))) {
|
||||
absl::ReaderMutexLock lock(&sessions_mutex_);
|
||||
for (auto it = sessions_.begin(); it != sessions_.end(); ++it) {
|
||||
it->second->RecordHeartBeatIfChanged();
|
||||
}
|
||||
shutdownMu_.Unlock();
|
||||
}
|
||||
shutdownMu_.Unlock();
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace cdc_ft
|
||||
267
cdc_stream/multi_session.h
Normal file
267
cdc_stream/multi_session.h
Normal 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.
|
||||
*/
|
||||
|
||||
#ifndef CDC_STREAM_MULTI_SESSION_H_
|
||||
#define CDC_STREAM_MULTI_SESSION_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "cdc_stream/asset_stream_server.h"
|
||||
#include "cdc_stream/metrics_recorder.h"
|
||||
#include "cdc_stream/session_config.h"
|
||||
#include "common/stopwatch.h"
|
||||
#include "data_store/data_store_writer.h"
|
||||
#include "manifest/file_chunk_map.h"
|
||||
#include "manifest/manifest_updater.h"
|
||||
|
||||
namespace cdc_ft {
|
||||
|
||||
class ProcessFactory;
|
||||
class Session;
|
||||
struct SessionTarget;
|
||||
using ManifestUpdatedCb = std::function<void()>;
|
||||
|
||||
// Updates the manifest and runs a file watcher in a background thread.
|
||||
class MultiSessionRunner {
|
||||
public:
|
||||
// |src_dir| is the source directory on the workstation to stream.
|
||||
// |data_store| can be passed for tests to override the default store used.
|
||||
// |process_factory| abstracts process creation.
|
||||
// |enable_stats| shows whether statistics should be derived.
|
||||
// |wait_duration| is the waiting time for changes in the streamed directory.
|
||||
// |num_updater_threads| is the thread count for the manifest updater.
|
||||
// |manifest_updated_cb| is the callback executed when a new manifest is set.
|
||||
MultiSessionRunner(
|
||||
std::string src_dir, DataStoreWriter* data_store,
|
||||
ProcessFactory* process_factory, bool enable_stats,
|
||||
absl::Duration wait_duration, uint32_t num_updater_threads,
|
||||
MultiSessionMetricsRecorder const* metrics_recorder,
|
||||
ManifestUpdatedCb manifest_updated_cb = ManifestUpdatedCb());
|
||||
|
||||
~MultiSessionRunner() = default;
|
||||
|
||||
// Starts |server_| of |type| on |port|.
|
||||
absl::Status Initialize(
|
||||
int port, AssetStreamServerType type,
|
||||
ContentSentHandler content_sent = ContentSentHandler());
|
||||
|
||||
// Stops updating the manifest and |server_|.
|
||||
absl::Status Shutdown() ABSL_LOCKS_EXCLUDED(mutex_);
|
||||
|
||||
// Waits until a manifest is ready and the session for |instance_id| has
|
||||
// acknowledged the reception of the currently set manifest id. |fuse_timeout|
|
||||
// is the timeout for waiting for the FUSE manifest ack. The time required to
|
||||
// generate the manifest is not part of this timeout as this could take a
|
||||
// longer time for a directory with many files.
|
||||
absl::Status WaitForManifestAck(const std::string& instance_id,
|
||||
absl::Duration fuse_timeout);
|
||||
|
||||
absl::Status Status() ABSL_LOCKS_EXCLUDED(mutex_);
|
||||
|
||||
// Returns the current manifest id used by |server_|.
|
||||
ContentIdProto ManifestId() const;
|
||||
|
||||
private:
|
||||
// Updates manifest if the content of the watched directory changes and
|
||||
// distributes it to subscribed gamelets.
|
||||
void Run();
|
||||
|
||||
// Record MultiSessionStart event.
|
||||
void RecordMultiSessionStart(const ManifestUpdater& manifest_updater);
|
||||
|
||||
// Record ManifestUpdate event.
|
||||
void RecordManifestUpdate(const ManifestUpdater& manifest_updater,
|
||||
absl::Duration duration,
|
||||
metrics::UpdateTrigger trigger,
|
||||
absl::Status status);
|
||||
|
||||
void SetStatus(absl::Status status) ABSL_LOCKS_EXCLUDED(mutex_);
|
||||
|
||||
// Files changed callback called from FileWatcherWin.
|
||||
void OnFilesChanged() ABSL_LOCKS_EXCLUDED(mutex_);
|
||||
|
||||
// Directory recreated callback called from FileWatcherWin.
|
||||
void OnDirRecreated() ABSL_LOCKS_EXCLUDED(mutex_);
|
||||
|
||||
// Called during manifest update when the intermediate manifest or the final
|
||||
// manifest is available. Pushes the manifest to connected FUSEs.
|
||||
void SetManifest(const ContentIdProto& manifest_id);
|
||||
|
||||
const std::string src_dir_;
|
||||
DataStoreWriter* const data_store_;
|
||||
ProcessFactory* const process_factory_;
|
||||
FileChunkMap file_chunks_;
|
||||
const absl::Duration wait_duration_;
|
||||
const uint32_t num_updater_threads_;
|
||||
const ManifestUpdatedCb manifest_updated_cb_;
|
||||
std::unique_ptr<AssetStreamServer> server_;
|
||||
std::unique_ptr<ManifestUpdater> manifest_updater_;
|
||||
|
||||
// Modifications (shutdown, file changes).
|
||||
absl::Mutex mutex_;
|
||||
bool shutdown_ ABSL_GUARDED_BY(mutex_) = false;
|
||||
bool files_changed_ ABSL_GUARDED_BY(mutex_) = false;
|
||||
bool dir_recreated_ ABSL_GUARDED_BY(mutex_) = false;
|
||||
bool manifest_set_ ABSL_GUARDED_BY(mutex_) = false;
|
||||
Stopwatch files_changed_timer_ ABSL_GUARDED_BY(mutex_);
|
||||
absl::Status status_ ABSL_GUARDED_BY(mutex_);
|
||||
|
||||
// Background thread that watches files and updates the manifest.
|
||||
std::unique_ptr<std::thread> thread_;
|
||||
|
||||
MultiSessionMetricsRecorder const* metrics_recorder_;
|
||||
};
|
||||
|
||||
// Manages an asset streaming session from a fixed directory on the workstation
|
||||
// to an arbitrary number of gamelets.
|
||||
class MultiSession {
|
||||
public:
|
||||
// Maximum length of cache path. We must be able to write content hashes into
|
||||
// this path:
|
||||
// <cache path>\01234567890123456789<null terminator> = 260 characters.
|
||||
static constexpr size_t kDefaultMaxCachePathLen =
|
||||
260 - 1 - ContentId::kHashSize * 2 - 1;
|
||||
|
||||
// Length of the hash appended to the cache directory, exposed for testing.
|
||||
static constexpr size_t kDirHashLen = 8;
|
||||
|
||||
// |src_dir| is the source directory on the workstation to stream.
|
||||
// |cfg| contains generic configuration parameters for each session.
|
||||
// |process_factory| abstracts process creation.
|
||||
// |data_store| can be passed for tests to override the default store used.
|
||||
// By default, the class uses a DiskDataStore that writes to
|
||||
// %APPDATA%\GGP\asset_streaming|<dir_derived_from_src_dir> on Windows.
|
||||
MultiSession(std::string src_dir, SessionConfig cfg,
|
||||
ProcessFactory* process_factory,
|
||||
MultiSessionMetricsRecorder const* metrics_recorder,
|
||||
std::unique_ptr<DataStoreWriter> data_store = nullptr);
|
||||
~MultiSession();
|
||||
|
||||
// Initializes the data store if not overridden in the constructor and starts
|
||||
// a background thread for updating the manifest and watching file changes.
|
||||
// Does not wait for the initial manifest update to finish. Use IsRunning()
|
||||
// to determine whether it is finished.
|
||||
// Not thread-safe.
|
||||
absl::Status Initialize();
|
||||
|
||||
// Stops all sessions and shuts down the server.
|
||||
// Not thread-safe.
|
||||
absl::Status Shutdown() ABSL_LOCKS_EXCLUDED(shutdownMu_);
|
||||
|
||||
// Returns the |src_dir| streaming directory passed to the constructor.
|
||||
const std::string& src_dir() const { return src_dir_; }
|
||||
|
||||
// Returns the status of the background thread.
|
||||
// Not thread-safe.
|
||||
absl::Status Status();
|
||||
|
||||
// Starts a new streaming session to the instance described by |target| and
|
||||
// waits until the FUSE has received the initial manifest id.
|
||||
// Returns an error if a session for that instance already exists.
|
||||
// |instance_id| is a unique id for the remote instance and mount directory,
|
||||
// e.g. user@host:mount_dir.
|
||||
// |target| identifies the remote target and how to connect to it.
|
||||
// |project_id| is the project that owns the instance. Stadia only.
|
||||
// |organization_id| is organization that contains the instance. Stadia only.
|
||||
// Thread-safe.
|
||||
absl::Status StartSession(const std::string& instance_id,
|
||||
const SessionTarget& target,
|
||||
const std::string& project_id,
|
||||
const std::string& organization_id)
|
||||
ABSL_LOCKS_EXCLUDED(sessions_mutex_);
|
||||
|
||||
// Stops the session for the given |instance_id|.
|
||||
// Returns a NotFound error if a session for that instance does not exists.
|
||||
// Thread-safe.
|
||||
absl::Status StopSession(const std::string& instance_id)
|
||||
ABSL_LOCKS_EXCLUDED(sessions_mutex_);
|
||||
|
||||
// Returns true if there is an existing session for |instance_id|.
|
||||
bool HasSession(const std::string& instance_id)
|
||||
ABSL_LOCKS_EXCLUDED(sessions_mutex_);
|
||||
|
||||
// Returns true if the FUSE process is up and running for an existing session
|
||||
// with ID |instance_id|.
|
||||
bool IsSessionHealthy(const std::string& instance_id)
|
||||
ABSL_LOCKS_EXCLUDED(sessions_mutex_);
|
||||
|
||||
// Returns true if the MultiSession does not have any active sessions.
|
||||
bool Empty() ABSL_LOCKS_EXCLUDED(sessions_mutex_);
|
||||
|
||||
// Returns the number of avtive sessions.
|
||||
uint32_t GetSessionCount() ABSL_LOCKS_EXCLUDED(sessions_mutex_);
|
||||
|
||||
// For a given source directory |dir|, e.g. "C:\path\to\game", returns a
|
||||
// sanitized version of |dir| plus a hash of |dir|, e.g.
|
||||
// "c__path_to_game_abcdef01".
|
||||
static std::string GetCacheDir(std::string dir);
|
||||
|
||||
// Returns the directory where manifest chunks are cached, e.g.
|
||||
// "%APPDATA%\GGP\asset_streaming\c__path_to_game_abcdef01" for
|
||||
// "C:\path\to\game".
|
||||
// The returned path is shortened to |max_len| by removing UTF8 code points
|
||||
// from the beginning of the actual cache directory (c__path...) if necessary.
|
||||
static absl::StatusOr<std::string> GetCachePath(
|
||||
const std::string& src_dir, size_t max_len = kDefaultMaxCachePathLen);
|
||||
|
||||
// Record an event associated with the multi-session.
|
||||
void RecordMultiSessionEvent(metrics::DeveloperLogEvent event,
|
||||
metrics::EventType code);
|
||||
|
||||
// Record an event for a session associated with the |instance_id|.
|
||||
void RecordSessionEvent(metrics::DeveloperLogEvent event,
|
||||
metrics::EventType code,
|
||||
const std::string& instance_id);
|
||||
|
||||
private:
|
||||
std::string src_dir_;
|
||||
SessionConfig cfg_;
|
||||
ProcessFactory* const process_factory_;
|
||||
std::unique_ptr<DataStoreWriter> data_store_;
|
||||
std::thread heartbeat_watcher_;
|
||||
absl::Mutex shutdownMu_;
|
||||
bool shutdown_ ABSL_GUARDED_BY(shutdownMu_) = false;
|
||||
|
||||
// Background thread for watching file changes and updating the manifest.
|
||||
std::unique_ptr<MultiSessionRunner> runner_;
|
||||
|
||||
// Local forwarding port for the asset stream service.
|
||||
int local_asset_stream_port_ = 0;
|
||||
|
||||
// Maps instance id to sessions.
|
||||
std::unordered_map<std::string, std::unique_ptr<Session>> sessions_
|
||||
ABSL_GUARDED_BY(sessions_mutex_);
|
||||
absl::Mutex sessions_mutex_;
|
||||
|
||||
MultiSessionMetricsRecorder const* metrics_recorder_;
|
||||
|
||||
Session* FindSession(const std::string& instance_id)
|
||||
ABSL_LOCKS_EXCLUDED(sessions_mutex_);
|
||||
|
||||
void OnContentSent(size_t byte_count, size_t chunck_count,
|
||||
std::string instance_id);
|
||||
|
||||
void StartHeartBeatCheck();
|
||||
};
|
||||
|
||||
} // namespace cdc_ft
|
||||
|
||||
#endif // CDC_STREAM_MULTI_SESSION_H_
|
||||
556
cdc_stream/multi_session_test.cc
Normal file
556
cdc_stream/multi_session_test.cc
Normal file
@@ -0,0 +1,556 @@
|
||||
// Copyright 2022 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "cdc_stream/multi_session.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/strings/match.h"
|
||||
#include "cdc_stream/testing_asset_stream_server.h"
|
||||
#include "common/path.h"
|
||||
#include "common/platform.h"
|
||||
#include "common/process.h"
|
||||
#include "common/status_test_macros.h"
|
||||
#include "common/test_main.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "manifest/manifest_test_base.h"
|
||||
|
||||
namespace cdc_ft {
|
||||
namespace {
|
||||
constexpr char kTestDir[] = "multisession_test_dir";
|
||||
constexpr char kData[] = {10, 20, 30, 40, 50, 60, 70, 80, 90};
|
||||
constexpr size_t kDataSize = sizeof(kData);
|
||||
constexpr char kInstance[] = "test_instance";
|
||||
constexpr int kPort = 44444;
|
||||
constexpr absl::Duration kTimeout = absl::Milliseconds(5);
|
||||
constexpr char kVeryLongPath[] =
|
||||
"C:\\this\\is\\some\\really\\really\\really\\really\\really\\really\\really"
|
||||
"\\really\\really\\really\\really\\really\\really\\really\\really\\really"
|
||||
"\\really\\really\\really\\really\\really\\really\\really\\really\\really"
|
||||
"\\really\\really\\really\\really\\really\\really\\really\\really\\really"
|
||||
"\\really\\long\\path";
|
||||
constexpr uint32_t kNumThreads = 1;
|
||||
|
||||
struct MetricsRecord {
|
||||
MetricsRecord(metrics::DeveloperLogEvent evt, metrics::EventType code)
|
||||
: evt(std::move(evt)), code(code) {}
|
||||
metrics::DeveloperLogEvent evt;
|
||||
metrics::EventType code;
|
||||
};
|
||||
|
||||
class MetricsServiceForTest : public MultiSessionMetricsRecorder {
|
||||
public:
|
||||
MetricsServiceForTest() : MultiSessionMetricsRecorder(nullptr) {}
|
||||
|
||||
virtual ~MetricsServiceForTest() = default;
|
||||
|
||||
void RecordEvent(metrics::DeveloperLogEvent event,
|
||||
metrics::EventType code) const override
|
||||
ABSL_LOCKS_EXCLUDED(mutex_) {
|
||||
absl::MutexLock lock(&mutex_);
|
||||
metrics_records_.push_back(MetricsRecord(std::move(event), code));
|
||||
}
|
||||
|
||||
// Waits until |num_events| events of type |type| have been recorded, or until
|
||||
// the function times out. Returns true if the condition was met and false if
|
||||
// in case of a timeout.
|
||||
bool WaitForEvents(metrics::EventType type, int num_events = 1,
|
||||
absl::Duration timeout = absl::Seconds(1)) {
|
||||
absl::MutexLock lock(&mutex_);
|
||||
auto cond = [this, type, num_events]() {
|
||||
return std::count_if(metrics_records_.begin(), metrics_records_.end(),
|
||||
[type](const MetricsRecord& mr) {
|
||||
return mr.code == type;
|
||||
}) >= num_events;
|
||||
};
|
||||
return mutex_.AwaitWithTimeout(absl::Condition(&cond), timeout);
|
||||
}
|
||||
|
||||
std::vector<MetricsRecord> GetEventsAndClear(metrics::EventType type)
|
||||
ABSL_LOCKS_EXCLUDED(mutex_) {
|
||||
std::vector<MetricsRecord> events;
|
||||
std::vector<MetricsRecord> remaining;
|
||||
absl::MutexLock lock(&mutex_);
|
||||
for (size_t i = 0; i < metrics_records_.size(); ++i) {
|
||||
if (metrics_records_[i].code == type) {
|
||||
events.push_back(std::move(metrics_records_[i]));
|
||||
} else {
|
||||
remaining.push_back(std::move(metrics_records_[i]));
|
||||
}
|
||||
}
|
||||
metrics_records_ = std::move(remaining);
|
||||
return events;
|
||||
}
|
||||
|
||||
private:
|
||||
mutable absl::Mutex mutex_;
|
||||
mutable std::vector<MetricsRecord> metrics_records_;
|
||||
};
|
||||
|
||||
class MultiSessionTest : public ManifestTestBase {
|
||||
public:
|
||||
MultiSessionTest() : ManifestTestBase(GetTestDataDir("multi_session")) {
|
||||
Log::Initialize(std::make_unique<ConsoleLog>(LogLevel::kInfo));
|
||||
}
|
||||
~MultiSessionTest() { Log::Shutdown(); }
|
||||
|
||||
void SetUp() override {
|
||||
// Use a temporary directory to be able to test empty directories (git does
|
||||
// not index empty directories) and creation/deletion of files.
|
||||
EXPECT_OK(path::RemoveDirRec(test_dir_path_));
|
||||
EXPECT_OK(path::CreateDirRec(test_dir_path_));
|
||||
metrics_service_ = new MetricsServiceForTest();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
EXPECT_OK(path::RemoveDirRec(test_dir_path_));
|
||||
delete metrics_service_;
|
||||
}
|
||||
|
||||
protected:
|
||||
// Callback if the manifest was updated == a new manifest is set.
|
||||
void OnManifestUpdated() ABSL_LOCKS_EXCLUDED(mutex_) {
|
||||
absl::MutexLock lock(&mutex_);
|
||||
++num_manifest_updates_;
|
||||
}
|
||||
|
||||
// Waits until the manifest is fully computed: the manifest id is not changed
|
||||
// anymore.
|
||||
bool WaitForManifestUpdated(uint32_t exp_num_manifest_updates,
|
||||
absl::Duration timeout = absl::Seconds(5)) {
|
||||
absl::MutexLock lock(&mutex_);
|
||||
auto cond = [&]() {
|
||||
return exp_num_manifest_updates == num_manifest_updates_;
|
||||
};
|
||||
mutex_.AwaitWithTimeout(absl::Condition(&cond), timeout);
|
||||
return exp_num_manifest_updates == num_manifest_updates_;
|
||||
}
|
||||
|
||||
void CheckMultiSessionStartNotRecorded() {
|
||||
std::vector<MetricsRecord> events = metrics_service_->GetEventsAndClear(
|
||||
metrics::EventType::kMultiSessionStart);
|
||||
EXPECT_EQ(events.size(), 0);
|
||||
}
|
||||
|
||||
void CheckMultiSessionStartRecorded(uint64_t byte_count, uint64_t chunk_count,
|
||||
uint32_t file_count) {
|
||||
std::vector<MetricsRecord> events = metrics_service_->GetEventsAndClear(
|
||||
metrics::EventType::kMultiSessionStart);
|
||||
ASSERT_EQ(events.size(), 1);
|
||||
metrics::MultiSessionStartData* data =
|
||||
events[0].evt.as_manager_data->multi_session_start_data.get();
|
||||
EXPECT_EQ(data->byte_count, byte_count);
|
||||
EXPECT_EQ(data->chunk_count, chunk_count);
|
||||
EXPECT_EQ(data->file_count, file_count);
|
||||
EXPECT_EQ(data->min_chunk_size, 128 << 10);
|
||||
EXPECT_EQ(data->avg_chunk_size, 256 << 10);
|
||||
EXPECT_EQ(data->max_chunk_size, 1024 << 10);
|
||||
}
|
||||
|
||||
metrics::ManifestUpdateData GetManifestUpdateData(
|
||||
metrics::UpdateTrigger trigger, absl::StatusCode status,
|
||||
size_t total_assets_added_or_updated, size_t total_assets_deleted,
|
||||
size_t total_chunks, size_t total_files_added_or_updated,
|
||||
size_t total_files_failed, size_t total_processed_bytes) {
|
||||
metrics::ManifestUpdateData manifest_upd;
|
||||
manifest_upd.trigger = trigger;
|
||||
manifest_upd.status = status;
|
||||
manifest_upd.total_assets_added_or_updated = total_assets_added_or_updated;
|
||||
manifest_upd.total_assets_deleted = total_assets_deleted;
|
||||
manifest_upd.total_chunks = total_chunks;
|
||||
manifest_upd.total_files_added_or_updated = total_files_added_or_updated;
|
||||
manifest_upd.total_files_failed = total_files_failed;
|
||||
manifest_upd.total_processed_bytes = total_processed_bytes;
|
||||
return manifest_upd;
|
||||
}
|
||||
|
||||
void CheckManifestUpdateRecorded(
|
||||
std::vector<metrics::ManifestUpdateData> manifests) {
|
||||
std::vector<MetricsRecord> events = metrics_service_->GetEventsAndClear(
|
||||
metrics::EventType::kManifestUpdated);
|
||||
ASSERT_EQ(events.size(), manifests.size());
|
||||
for (size_t i = 0; i < manifests.size(); ++i) {
|
||||
metrics::ManifestUpdateData* data =
|
||||
events[i].evt.as_manager_data->manifest_update_data.get();
|
||||
EXPECT_LT(data->local_duration_ms, 60000ull);
|
||||
EXPECT_EQ(data->status, manifests[i].status);
|
||||
EXPECT_EQ(data->total_assets_added_or_updated,
|
||||
manifests[i].total_assets_added_or_updated);
|
||||
EXPECT_EQ(data->total_assets_deleted, manifests[i].total_assets_deleted);
|
||||
EXPECT_EQ(data->total_chunks, manifests[i].total_chunks);
|
||||
EXPECT_EQ(data->total_files_added_or_updated,
|
||||
manifests[i].total_files_added_or_updated);
|
||||
EXPECT_EQ(data->total_processed_bytes,
|
||||
manifests[i].total_processed_bytes);
|
||||
EXPECT_EQ(data->trigger, manifests[i].trigger);
|
||||
}
|
||||
}
|
||||
|
||||
const std::string test_dir_path_ = path::Join(path::GetTempDir(), kTestDir);
|
||||
WinProcessFactory process_factory_;
|
||||
absl::Mutex mutex_;
|
||||
uint32_t num_manifest_updates_ ABSL_GUARDED_BY(mutex_) = 0;
|
||||
MetricsServiceForTest* metrics_service_;
|
||||
};
|
||||
|
||||
constexpr char kCacheDir[] = "c__path_to_dir_ee54bbbc";
|
||||
|
||||
TEST_F(MultiSessionTest, GetCacheDir_IgnoresTrailingPathSeparators) {
|
||||
EXPECT_EQ(MultiSession::GetCacheDir("C:\\path\\to\\dir"), kCacheDir);
|
||||
EXPECT_EQ(MultiSession::GetCacheDir("C:\\path\\to\\dir\\"), kCacheDir);
|
||||
}
|
||||
|
||||
TEST_F(MultiSessionTest, GetCacheDir_WorksWithForwardSlashes) {
|
||||
EXPECT_EQ(MultiSession::GetCacheDir("C:/path/to/dir"), kCacheDir);
|
||||
EXPECT_EQ(MultiSession::GetCacheDir("C:/path/to/dir/"), kCacheDir);
|
||||
}
|
||||
|
||||
TEST_F(MultiSessionTest, GetCacheDir_ReplacesInvalidCharacters) {
|
||||
EXPECT_EQ(MultiSession::GetCacheDir("C:\\<>:\"/\\|?*"),
|
||||
"c___________ae188efd");
|
||||
}
|
||||
|
||||
TEST_F(MultiSessionTest, GetCacheDir_UsesFullPath) {
|
||||
EXPECT_EQ(MultiSession::GetCacheDir("foo/bar"),
|
||||
MultiSession::GetCacheDir(path::GetFullPath("foo/bar")));
|
||||
}
|
||||
|
||||
#if PLATFORM_WINDOWS
|
||||
TEST_F(MultiSessionTest, GetCacheDir_IgnoresCaseOnWindows) {
|
||||
EXPECT_EQ(MultiSession::GetCacheDir("C:\\PATH\\TO\\DIR"), kCacheDir);
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST_F(MultiSessionTest, GetCachePath_ContainsExpectedParts) {
|
||||
absl::StatusOr<std::string> cache_path =
|
||||
MultiSession::GetCachePath("C:\\path\\to\\dir");
|
||||
ASSERT_OK(cache_path);
|
||||
EXPECT_TRUE(absl::EndsWith(*cache_path, kCacheDir)) << *cache_path;
|
||||
EXPECT_TRUE(
|
||||
absl::StrContains(*cache_path, path::Join("GGP", "asset_streaming")))
|
||||
<< *cache_path;
|
||||
}
|
||||
|
||||
TEST_F(MultiSessionTest, GetCachePath_ShortensLongPaths) {
|
||||
EXPECT_GT(strlen(kVeryLongPath), MultiSession::kDefaultMaxCachePathLen);
|
||||
std::string cache_dir = MultiSession::GetCacheDir(kVeryLongPath);
|
||||
absl::StatusOr<std::string> cache_path =
|
||||
MultiSession::GetCachePath(kVeryLongPath);
|
||||
ASSERT_OK(cache_path);
|
||||
EXPECT_EQ(cache_path->size(), MultiSession::kDefaultMaxCachePathLen);
|
||||
EXPECT_TRUE(
|
||||
absl::StrContains(*cache_path, path::Join("GGP", "asset_streaming")))
|
||||
<< *cache_path;
|
||||
// The hash in the end of the path is kept and not shortened.
|
||||
EXPECT_EQ(cache_dir.substr(cache_dir.size() - MultiSession::kDirHashLen),
|
||||
cache_path->substr(cache_path->size() - MultiSession::kDirHashLen));
|
||||
}
|
||||
|
||||
TEST_F(MultiSessionTest, GetCachePath_DoesNotSplitUtfCodePoints) {
|
||||
// Find out the length of the %APPDATA%\GGP\asset_streaming\" + hash part.
|
||||
absl::StatusOr<std::string> cache_path = MultiSession::GetCachePath("");
|
||||
ASSERT_OK(cache_path);
|
||||
size_t base_len = cache_path->size();
|
||||
|
||||
// Path has are two 2-byte characters. They should not be split in the middle.
|
||||
cache_path = MultiSession::GetCachePath(u8"\u0200\u0200", base_len);
|
||||
ASSERT_OK(cache_path);
|
||||
EXPECT_EQ(cache_path->size(), base_len);
|
||||
|
||||
// %APPDATA%\GGP\asset_streaming\abcdefg
|
||||
cache_path = MultiSession::GetCachePath(u8"\u0200\u0200", base_len + 1);
|
||||
ASSERT_OK(cache_path);
|
||||
EXPECT_EQ(cache_path->size(), base_len);
|
||||
|
||||
// %APPDATA%\GGP\asset_streaming\\u0200abcdefg
|
||||
cache_path = MultiSession::GetCachePath(u8"\u0200\u0200", base_len + 2);
|
||||
ASSERT_OK(cache_path);
|
||||
EXPECT_EQ(cache_path->size(), base_len + 2);
|
||||
|
||||
// %APPDATA%\GGP\asset_streaming\\u0200abcdefg
|
||||
cache_path = MultiSession::GetCachePath(u8"\u0200\u0200", base_len + 3);
|
||||
ASSERT_OK(cache_path);
|
||||
EXPECT_EQ(cache_path->size(), base_len + 2);
|
||||
}
|
||||
|
||||
// Calculate manifest for an empty directory.
|
||||
TEST_F(MultiSessionTest, MultiSessionRunnerOnEmpty) {
|
||||
cfg_.src_dir = test_dir_path_;
|
||||
MultiSessionRunner runner(cfg_.src_dir, &data_store_, &process_factory_,
|
||||
/*enable_stats=*/false, kTimeout, kNumThreads,
|
||||
metrics_service_,
|
||||
[this]() { OnManifestUpdated(); });
|
||||
EXPECT_OK(runner.Initialize(kPort, AssetStreamServerType::kTest));
|
||||
EXPECT_TRUE(WaitForManifestUpdated(2));
|
||||
ASSERT_TRUE(
|
||||
metrics_service_->WaitForEvents(metrics::EventType::kMultiSessionStart));
|
||||
ASSERT_NO_FATAL_FAILURE(ExpectManifestEquals({}, runner.ManifestId()));
|
||||
CheckMultiSessionStartRecorded(0, 0, 0);
|
||||
CheckManifestUpdateRecorded(std::vector<metrics::ManifestUpdateData>{
|
||||
GetManifestUpdateData(metrics::UpdateTrigger::kInitUpdateAll,
|
||||
absl::StatusCode::kOk, 0, 0, 0, 0, 0, 0)});
|
||||
|
||||
EXPECT_OK(runner.Status());
|
||||
EXPECT_OK(runner.Shutdown());
|
||||
}
|
||||
|
||||
// Calculate manifest for a non-empty directory.
|
||||
TEST_F(MultiSessionTest, MultiSessionRunnerNonEmptySucceeds) {
|
||||
// Contains a.txt, subdir/b.txt, subdir/c.txt, subdir/d.txt.
|
||||
cfg_.src_dir = path::Join(base_dir_, "non_empty");
|
||||
MultiSessionRunner runner(cfg_.src_dir, &data_store_, &process_factory_,
|
||||
/*enable_stats=*/false, kTimeout, kNumThreads,
|
||||
metrics_service_,
|
||||
[this]() { OnManifestUpdated(); });
|
||||
EXPECT_OK(runner.Initialize(kPort, AssetStreamServerType::kTest));
|
||||
EXPECT_TRUE(WaitForManifestUpdated(2));
|
||||
ASSERT_TRUE(
|
||||
metrics_service_->WaitForEvents(metrics::EventType::kMultiSessionStart));
|
||||
CheckMultiSessionStartRecorded(46, 4, 4);
|
||||
ASSERT_NO_FATAL_FAILURE(ExpectManifestEquals(
|
||||
{"a.txt", "subdir", "subdir/b.txt", "subdir/c.txt", "subdir/d.txt"},
|
||||
runner.ManifestId()));
|
||||
EXPECT_OK(runner.Status());
|
||||
EXPECT_OK(runner.Shutdown());
|
||||
}
|
||||
|
||||
// Update manifest on adding a file.
|
||||
TEST_F(MultiSessionTest, MultiSessionRunnerAddFileSucceeds) {
|
||||
cfg_.src_dir = test_dir_path_;
|
||||
MultiSessionRunner runner(cfg_.src_dir, &data_store_, &process_factory_,
|
||||
/*enable_stats=*/false, kTimeout, kNumThreads,
|
||||
metrics_service_,
|
||||
[this]() { OnManifestUpdated(); });
|
||||
|
||||
{
|
||||
SCOPED_TRACE("Initialize.");
|
||||
|
||||
EXPECT_OK(runner.Initialize(kPort, AssetStreamServerType::kTest));
|
||||
// 1 file was added, 1 intermediate + 1 final manifest is pushed.
|
||||
EXPECT_TRUE(WaitForManifestUpdated(2));
|
||||
EXPECT_OK(runner.WaitForManifestAck(kInstance, kTimeout));
|
||||
EXPECT_TRUE(metrics_service_->WaitForEvents(
|
||||
metrics::EventType::kMultiSessionStart));
|
||||
ASSERT_OK(runner.Status());
|
||||
}
|
||||
|
||||
{
|
||||
SCOPED_TRACE("Created base manifest for the test directory.");
|
||||
|
||||
CheckMultiSessionStartRecorded(0, 0, 0);
|
||||
ASSERT_NO_FATAL_FAILURE(ExpectManifestEquals({}, runner.ManifestId()));
|
||||
CheckManifestUpdateRecorded(std::vector<metrics::ManifestUpdateData>{
|
||||
GetManifestUpdateData(metrics::UpdateTrigger::kInitUpdateAll,
|
||||
absl::StatusCode::kOk, 0, 0, 0, 0, 0, 0)});
|
||||
}
|
||||
|
||||
{
|
||||
SCOPED_TRACE("Added file.txt.");
|
||||
|
||||
uint32_t prev_updates = num_manifest_updates_;
|
||||
const std::string file_path = path::Join(test_dir_path_, "file.txt");
|
||||
EXPECT_OK(path::WriteFile(file_path, kData, kDataSize));
|
||||
// 1 file was added, 1 intermediate + 1 final manifest is pushed.
|
||||
EXPECT_TRUE(WaitForManifestUpdated(prev_updates + 2));
|
||||
EXPECT_TRUE(
|
||||
metrics_service_->WaitForEvents(metrics::EventType::kManifestUpdated));
|
||||
ASSERT_NO_FATAL_FAILURE(
|
||||
ExpectManifestEquals({"file.txt"}, runner.ManifestId()));
|
||||
CheckMultiSessionStartNotRecorded();
|
||||
CheckManifestUpdateRecorded(
|
||||
std::vector<metrics::ManifestUpdateData>{GetManifestUpdateData(
|
||||
metrics::UpdateTrigger::kRegularUpdate, absl::StatusCode::kOk, 1, 0,
|
||||
1, 1, 0, kDataSize)});
|
||||
}
|
||||
EXPECT_OK(runner.Status());
|
||||
EXPECT_OK(runner.Shutdown());
|
||||
}
|
||||
|
||||
// Fail if the directory does not exist as the watching could not be started.
|
||||
// At this moment we expect that the directory exists.
|
||||
TEST_F(MultiSessionTest, MultiSessionRunnerNoDirFails) {
|
||||
cfg_.src_dir = path::Join(base_dir_, "non_existing");
|
||||
MultiSessionRunner runner(cfg_.src_dir, &data_store_, &process_factory_,
|
||||
/*enable_stats=*/false, kTimeout, kNumThreads,
|
||||
metrics_service_,
|
||||
[this]() { OnManifestUpdated(); });
|
||||
EXPECT_OK(runner.Initialize(kPort, AssetStreamServerType::kTest));
|
||||
|
||||
ASSERT_FALSE(
|
||||
absl::IsNotFound(runner.WaitForManifestAck(kInstance, kTimeout)));
|
||||
ASSERT_FALSE(WaitForManifestUpdated(1, absl::Milliseconds(10)));
|
||||
CheckMultiSessionStartNotRecorded();
|
||||
CheckManifestUpdateRecorded(std::vector<metrics::ManifestUpdateData>{});
|
||||
EXPECT_NOT_OK(runner.Shutdown());
|
||||
EXPECT_TRUE(absl::StrContains(runner.Status().ToString(),
|
||||
"Could not start watching"));
|
||||
}
|
||||
|
||||
// Do not break if the directory is recreated.
|
||||
TEST_F(MultiSessionTest, MultiSessionRunnerDirRecreatedSucceeds) {
|
||||
cfg_.src_dir = test_dir_path_;
|
||||
EXPECT_OK(path::WriteFile(path::Join(test_dir_path_, "file.txt"), kData,
|
||||
kDataSize));
|
||||
|
||||
MultiSessionRunner runner(cfg_.src_dir, &data_store_, &process_factory_,
|
||||
/*enable_stats=*/false, kTimeout, kNumThreads,
|
||||
metrics_service_,
|
||||
[this]() { OnManifestUpdated(); });
|
||||
EXPECT_OK(runner.Initialize(kPort, AssetStreamServerType::kTest));
|
||||
|
||||
{
|
||||
SCOPED_TRACE("Originally, only the streamed directory contains file.txt.");
|
||||
EXPECT_TRUE(WaitForManifestUpdated(2));
|
||||
ASSERT_TRUE(metrics_service_->WaitForEvents(
|
||||
metrics::EventType::kMultiSessionStart));
|
||||
CheckMultiSessionStartRecorded((uint64_t)kDataSize, 1, 1);
|
||||
ASSERT_NO_FATAL_FAILURE(
|
||||
ExpectManifestEquals({"file.txt"}, runner.ManifestId()));
|
||||
CheckManifestUpdateRecorded(
|
||||
std::vector<metrics::ManifestUpdateData>{GetManifestUpdateData(
|
||||
metrics::UpdateTrigger::kInitUpdateAll, absl::StatusCode::kOk, 1, 0,
|
||||
1, 1, 0, kDataSize)});
|
||||
}
|
||||
|
||||
{
|
||||
SCOPED_TRACE(
|
||||
"Remove the streamed directory, the manifest should become empty.");
|
||||
uint32_t prev_updates = num_manifest_updates_;
|
||||
EXPECT_OK(path::RemoveDirRec(test_dir_path_));
|
||||
ASSERT_TRUE(WaitForManifestUpdated(prev_updates + 1));
|
||||
ASSERT_NO_FATAL_FAILURE(ExpectManifestEquals({}, runner.ManifestId()));
|
||||
CheckManifestUpdateRecorded(
|
||||
std::vector<metrics::ManifestUpdateData>{GetManifestUpdateData(
|
||||
metrics::UpdateTrigger::kRunningUpdateAll,
|
||||
absl::StatusCode::kNotFound, 1, 0, 1, 1, 0, kDataSize)});
|
||||
}
|
||||
|
||||
{
|
||||
SCOPED_TRACE(
|
||||
"Create the watched directory -> an empty manifest should be "
|
||||
"streamed.");
|
||||
uint32_t prev_updates = num_manifest_updates_;
|
||||
EXPECT_OK(path::CreateDirRec(test_dir_path_));
|
||||
// The first update is always the empty manifest, wait for the second one.
|
||||
EXPECT_TRUE(WaitForManifestUpdated(prev_updates + 2));
|
||||
ASSERT_NO_FATAL_FAILURE(ExpectManifestEquals({}, runner.ManifestId()));
|
||||
EXPECT_TRUE(
|
||||
metrics_service_->WaitForEvents(metrics::EventType::kManifestUpdated));
|
||||
CheckManifestUpdateRecorded(std::vector<metrics::ManifestUpdateData>{
|
||||
GetManifestUpdateData(metrics::UpdateTrigger::kRunningUpdateAll,
|
||||
absl::StatusCode::kOk, 0, 0, 0, 0, 0, 0)});
|
||||
}
|
||||
|
||||
{
|
||||
SCOPED_TRACE("Create 'new_file.txt' -> new manifest should be created.");
|
||||
uint32_t prev_updates = num_manifest_updates_;
|
||||
EXPECT_OK(path::WriteFile(path::Join(test_dir_path_, "new_file.txt"), kData,
|
||||
kDataSize));
|
||||
// The first update doesn't have the chunks for new_file.txt, wait for the
|
||||
// second one.
|
||||
ASSERT_TRUE(WaitForManifestUpdated(prev_updates + 2));
|
||||
ASSERT_NO_FATAL_FAILURE(
|
||||
ExpectManifestEquals({"new_file.txt"}, runner.ManifestId()));
|
||||
EXPECT_TRUE(
|
||||
metrics_service_->WaitForEvents(metrics::EventType::kManifestUpdated));
|
||||
CheckManifestUpdateRecorded(
|
||||
std::vector<metrics::ManifestUpdateData>{GetManifestUpdateData(
|
||||
metrics::UpdateTrigger::kRegularUpdate, absl::StatusCode::kOk, 1, 0,
|
||||
1, 1, 0, kDataSize)});
|
||||
CheckMultiSessionStartNotRecorded();
|
||||
}
|
||||
|
||||
EXPECT_OK(runner.Status());
|
||||
EXPECT_OK(runner.Shutdown());
|
||||
}
|
||||
|
||||
// Fail if the streamed source is a file.
|
||||
TEST_F(MultiSessionTest, MultiSessionRunnerFileAsStreamedDirFails) {
|
||||
cfg_.src_dir = path::Join(test_dir_path_, "file.txt");
|
||||
EXPECT_OK(path::WriteFile(cfg_.src_dir, kData, kDataSize));
|
||||
|
||||
MultiSessionRunner runner(cfg_.src_dir, &data_store_, &process_factory_,
|
||||
/*enable_stats=*/false, kTimeout, kNumThreads,
|
||||
metrics_service_,
|
||||
[this]() { OnManifestUpdated(); });
|
||||
EXPECT_OK(runner.Initialize(kPort, AssetStreamServerType::kTest));
|
||||
ASSERT_FALSE(WaitForManifestUpdated(1, absl::Milliseconds(100)));
|
||||
CheckMultiSessionStartNotRecorded();
|
||||
CheckManifestUpdateRecorded(std::vector<metrics::ManifestUpdateData>{});
|
||||
EXPECT_NOT_OK(runner.Shutdown());
|
||||
EXPECT_TRUE(absl::StrContains(runner.Status().ToString(),
|
||||
"Failed to update manifest"))
|
||||
<< runner.Status().ToString();
|
||||
}
|
||||
|
||||
// Stream an empty manifest if the streamed directory was re-created as a file.
|
||||
TEST_F(MultiSessionTest,
|
||||
MultiSessionRunnerDirRecreatedAsFileSucceedsWithEmptyManifest) {
|
||||
cfg_.src_dir = path::Join(test_dir_path_, "file");
|
||||
EXPECT_OK(path::CreateDirRec(cfg_.src_dir));
|
||||
|
||||
MultiSessionRunner runner(cfg_.src_dir, &data_store_, &process_factory_,
|
||||
/*enable_stats=*/false, kTimeout, kNumThreads,
|
||||
metrics_service_,
|
||||
[this]() { OnManifestUpdated(); });
|
||||
{
|
||||
SCOPED_TRACE("Initialize manifest in test directory.");
|
||||
|
||||
EXPECT_OK(runner.Initialize(kPort, AssetStreamServerType::kTest));
|
||||
ASSERT_TRUE(WaitForManifestUpdated(2));
|
||||
ASSERT_TRUE(metrics_service_->WaitForEvents(
|
||||
metrics::EventType::kMultiSessionStart));
|
||||
CheckMultiSessionStartRecorded(0, 0, 0);
|
||||
CheckManifestUpdateRecorded(std::vector<metrics::ManifestUpdateData>{
|
||||
GetManifestUpdateData(metrics::UpdateTrigger::kInitUpdateAll,
|
||||
absl::StatusCode::kOk, 0, 0, 0, 0, 0, 0)});
|
||||
ASSERT_NO_FATAL_FAILURE(ExpectManifestEquals({}, runner.ManifestId()));
|
||||
}
|
||||
|
||||
{
|
||||
SCOPED_TRACE("Remove the streamed directory, the manifest becomes empty.");
|
||||
|
||||
uint32_t prev_updates = num_manifest_updates_;
|
||||
EXPECT_OK(path::RemoveDirRec(cfg_.src_dir));
|
||||
ASSERT_TRUE(WaitForManifestUpdated(prev_updates + 1));
|
||||
ASSERT_NO_FATAL_FAILURE(ExpectManifestEquals({}, runner.ManifestId()));
|
||||
CheckManifestUpdateRecorded(std::vector<metrics::ManifestUpdateData>{
|
||||
GetManifestUpdateData(metrics::UpdateTrigger::kRunningUpdateAll,
|
||||
absl::StatusCode::kNotFound, 0, 0, 0, 0, 0, 0)});
|
||||
}
|
||||
|
||||
{
|
||||
SCOPED_TRACE("Create a file in place of the directory");
|
||||
|
||||
uint32_t prev_updates = num_manifest_updates_;
|
||||
EXPECT_OK(path::WriteFile(cfg_.src_dir, kData, kDataSize));
|
||||
ASSERT_TRUE(WaitForManifestUpdated(prev_updates + 2));
|
||||
ASSERT_NO_FATAL_FAILURE(ExpectManifestEquals({}, runner.ManifestId()));
|
||||
metrics::ManifestUpdateData update_data = GetManifestUpdateData(
|
||||
metrics::UpdateTrigger::kRunningUpdateAll,
|
||||
absl::StatusCode::kFailedPrecondition, 0, 0, 0, 0, 0, 0);
|
||||
CheckManifestUpdateRecorded(
|
||||
std::vector<metrics::ManifestUpdateData>{update_data, update_data});
|
||||
CheckMultiSessionStartNotRecorded();
|
||||
}
|
||||
|
||||
EXPECT_OK(runner.Status());
|
||||
EXPECT_OK(runner.Shutdown());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace cdc_ft
|
||||
138
cdc_stream/session.cc
Normal file
138
cdc_stream/session.cc
Normal file
@@ -0,0 +1,138 @@
|
||||
// Copyright 2022 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "cdc_stream/session.h"
|
||||
|
||||
#include "cdc_stream/cdc_fuse_manager.h"
|
||||
#include "common/log.h"
|
||||
#include "common/port_manager.h"
|
||||
#include "common/status.h"
|
||||
#include "common/status_macros.h"
|
||||
#include "metrics/enums.h"
|
||||
#include "metrics/messages.h"
|
||||
|
||||
namespace cdc_ft {
|
||||
namespace {
|
||||
|
||||
// Timeout for initial gamelet connection.
|
||||
constexpr double kInstanceConnectionTimeoutSec = 60.0f;
|
||||
|
||||
metrics::DeveloperLogEvent GetEventWithHeartBeatData(size_t bytes,
|
||||
size_t chunks) {
|
||||
metrics::DeveloperLogEvent evt;
|
||||
evt.as_manager_data = std::make_unique<metrics::AssetStreamingManagerData>();
|
||||
evt.as_manager_data->session_data = std::make_unique<metrics::SessionData>();
|
||||
evt.as_manager_data->session_data->byte_count = bytes;
|
||||
evt.as_manager_data->session_data->chunk_count = chunks;
|
||||
return std::move(evt);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Session::Session(std::string instance_id, const SessionTarget& target,
|
||||
SessionConfig cfg, ProcessFactory* process_factory,
|
||||
std::unique_ptr<SessionMetricsRecorder> metrics_recorder)
|
||||
: instance_id_(std::move(instance_id)),
|
||||
mount_dir_(target.mount_dir),
|
||||
cfg_(std::move(cfg)),
|
||||
process_factory_(process_factory),
|
||||
remote_util_(cfg_.verbosity, cfg_.quiet, process_factory,
|
||||
/*forward_output_to_logging=*/true),
|
||||
metrics_recorder_(std::move(metrics_recorder)) {
|
||||
assert(metrics_recorder_);
|
||||
remote_util_.SetUserHostAndPort(target.user_host, target.ssh_port);
|
||||
if (!target.ssh_command.empty()) {
|
||||
remote_util_.SetSshCommand(target.ssh_command);
|
||||
}
|
||||
if (!target.scp_command.empty()) {
|
||||
remote_util_.SetScpCommand(target.scp_command);
|
||||
}
|
||||
}
|
||||
|
||||
Session::~Session() {
|
||||
absl::Status status = Stop();
|
||||
if (!status.ok()) {
|
||||
LOG_ERROR("Failed to stop session for instance '%s': %s", instance_id_,
|
||||
status.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
absl::Status Session::Start(int local_port, int first_remote_port,
|
||||
int last_remote_port) {
|
||||
// Find an available remote port.
|
||||
std::unordered_set<int> ports;
|
||||
ASSIGN_OR_RETURN(
|
||||
ports,
|
||||
PortManager::FindAvailableRemotePorts(
|
||||
first_remote_port, last_remote_port, "127.0.0.1", process_factory_,
|
||||
&remote_util_, kInstanceConnectionTimeoutSec),
|
||||
"Failed to find an available remote port in the range [%d, %d]",
|
||||
first_remote_port, last_remote_port);
|
||||
assert(!ports.empty());
|
||||
int remote_port = *ports.begin();
|
||||
|
||||
assert(!fuse_);
|
||||
fuse_ = std::make_unique<CdcFuseManager>(instance_id_, process_factory_,
|
||||
&remote_util_);
|
||||
RETURN_IF_ERROR(
|
||||
fuse_->Start(mount_dir_, local_port, remote_port, cfg_.verbosity,
|
||||
cfg_.fuse_debug, cfg_.fuse_singlethreaded, cfg_.stats,
|
||||
cfg_.fuse_check, cfg_.fuse_cache_capacity,
|
||||
cfg_.fuse_cleanup_timeout_sec,
|
||||
cfg_.fuse_access_idle_timeout_sec),
|
||||
"Failed to start instance component");
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status Session::Stop() {
|
||||
absl::ReaderMutexLock lock(&transferred_data_mu_);
|
||||
// TODO: Record error on session end.
|
||||
metrics_recorder_->RecordEvent(
|
||||
GetEventWithHeartBeatData(transferred_bytes_, transferred_chunks_),
|
||||
metrics::EventType::kSessionEnd);
|
||||
if (fuse_) {
|
||||
RETURN_IF_ERROR(fuse_->Stop());
|
||||
fuse_.reset();
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
bool Session::IsHealthy() { return fuse_->IsHealthy(); }
|
||||
|
||||
void Session::RecordEvent(metrics::DeveloperLogEvent event,
|
||||
metrics::EventType code) const {
|
||||
metrics_recorder_->RecordEvent(std::move(event), code);
|
||||
}
|
||||
|
||||
void Session::OnContentSent(size_t bytes, size_t chunks) {
|
||||
absl::WriterMutexLock lock(&transferred_data_mu_);
|
||||
transferred_bytes_ += bytes;
|
||||
transferred_chunks_ += chunks;
|
||||
}
|
||||
|
||||
void Session::RecordHeartBeatIfChanged() {
|
||||
absl::ReaderMutexLock lock(&transferred_data_mu_);
|
||||
if (transferred_bytes_ == last_read_bytes_ &&
|
||||
transferred_chunks_ == last_read_chunks_) {
|
||||
return;
|
||||
}
|
||||
last_read_bytes_ = transferred_bytes_;
|
||||
last_read_chunks_ = transferred_chunks_;
|
||||
metrics_recorder_->RecordEvent(
|
||||
GetEventWithHeartBeatData(last_read_bytes_, last_read_chunks_),
|
||||
metrics::EventType::kSessionHeartBeat);
|
||||
}
|
||||
|
||||
} // namespace cdc_ft
|
||||
103
cdc_stream/session.h
Normal file
103
cdc_stream/session.h
Normal 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 CDC_STREAM_SESSION_H_
|
||||
#define CDC_STREAM_SESSION_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "cdc_stream/metrics_recorder.h"
|
||||
#include "cdc_stream/session_config.h"
|
||||
#include "common/remote_util.h"
|
||||
|
||||
namespace cdc_ft {
|
||||
|
||||
class CdcFuseManager;
|
||||
class ProcessFactory;
|
||||
class Process;
|
||||
|
||||
// Defines a remote target and how to connect to it.
|
||||
struct SessionTarget {
|
||||
// SSH username and hostname of the remote target, formed as [user@]host.
|
||||
std::string user_host;
|
||||
// Port to use for SSH connections to the remote target.
|
||||
uint16_t ssh_port;
|
||||
// Ssh command to use to connect to the remote target.
|
||||
std::string ssh_command;
|
||||
// Scp command to use to copy files to the remote target.
|
||||
std::string scp_command;
|
||||
// Directory on the remote target where to mount the streamed directory.
|
||||
std::string mount_dir;
|
||||
};
|
||||
|
||||
// Manages the connection of a workstation to a single remote instance.
|
||||
class Session {
|
||||
public:
|
||||
// |instance_id| is a unique id for the remote instance.
|
||||
// |target| identifies the remote target and how to connect to it.
|
||||
// |cfg| contains generic configuration parameters for the session.
|
||||
// |process_factory| abstracts process creation.
|
||||
Session(std::string instance_id, const SessionTarget& target,
|
||||
SessionConfig cfg, ProcessFactory* process_factory,
|
||||
std::unique_ptr<SessionMetricsRecorder> metrics_recorder);
|
||||
~Session();
|
||||
|
||||
// Starts the CDC FUSE on the instance with established port forwarding.
|
||||
// |local_port| is the local reverse forwarding port to use.
|
||||
// [|first_remote_port|, |last_remote_port|] are the allowed remote ports.
|
||||
absl::Status Start(int local_port, int first_remote_port,
|
||||
int last_remote_port);
|
||||
|
||||
// Shuts down the connection to the instance.
|
||||
absl::Status Stop() ABSL_LOCKS_EXCLUDED(transferred_data_mu_);
|
||||
|
||||
// Returns true if the FUSE process is running.
|
||||
bool IsHealthy();
|
||||
|
||||
// Record an event for the session.
|
||||
void RecordEvent(metrics::DeveloperLogEvent event,
|
||||
metrics::EventType code) const;
|
||||
|
||||
// Is called when content was sent during the session.
|
||||
void OnContentSent(size_t bytes, size_t chunks)
|
||||
ABSL_LOCKS_EXCLUDED(transferred_data_mu_);
|
||||
|
||||
// Records heart beat data if it has changed since last record.
|
||||
void RecordHeartBeatIfChanged() ABSL_LOCKS_EXCLUDED(transferred_data_mu_);
|
||||
|
||||
private:
|
||||
const std::string instance_id_;
|
||||
const std::string mount_dir_;
|
||||
const SessionConfig cfg_;
|
||||
ProcessFactory* const process_factory_;
|
||||
|
||||
RemoteUtil remote_util_;
|
||||
std::unique_ptr<CdcFuseManager> fuse_;
|
||||
std::unique_ptr<SessionMetricsRecorder> metrics_recorder_;
|
||||
|
||||
absl::Mutex transferred_data_mu_;
|
||||
uint64_t transferred_bytes_ ABSL_GUARDED_BY(transferred_data_mu_) = 0;
|
||||
uint64_t transferred_chunks_ ABSL_GUARDED_BY(transferred_data_mu_) = 0;
|
||||
uint64_t last_read_bytes_ = 0;
|
||||
uint64_t last_read_chunks_ = 0;
|
||||
};
|
||||
|
||||
} // namespace cdc_ft
|
||||
|
||||
#endif // CDC_STREAM_SESSION_H_
|
||||
63
cdc_stream/session_config.h
Normal file
63
cdc_stream/session_config.h
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2022 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef CDC_STREAM_SESSION_CONFIG_H_
|
||||
#define CDC_STREAM_SESSION_CONFIG_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace cdc_ft {
|
||||
|
||||
// The values set in this config do not necessarily denote the default values.
|
||||
// For the defaults, see the corresponding flag values.
|
||||
struct SessionConfig {
|
||||
// General log verbosity.
|
||||
int verbosity = 0;
|
||||
|
||||
// Silence logs from process execution.
|
||||
bool quiet = false;
|
||||
|
||||
// Print detailed streaming stats.
|
||||
bool stats = false;
|
||||
|
||||
// Whether to run FUSE in debug mode.
|
||||
bool fuse_debug = false;
|
||||
|
||||
// Whether to run FUSE in single-threaded mode.
|
||||
bool fuse_singlethreaded = false;
|
||||
|
||||
// Whether to run FUSE consistency check.
|
||||
bool fuse_check = false;
|
||||
|
||||
// Cache capacity with a suffix.
|
||||
uint64_t fuse_cache_capacity = 0;
|
||||
|
||||
// Cleanup timeout in seconds.
|
||||
uint32_t fuse_cleanup_timeout_sec = 0;
|
||||
|
||||
// Access idling timeout in seconds.
|
||||
uint32_t fuse_access_idle_timeout_sec = 0;
|
||||
|
||||
// Number of threads used in the manifest updater to compute chunks/hashes.
|
||||
uint32_t manifest_updater_threads = 0;
|
||||
|
||||
// Time to wait until running a manifest update after detecting a file change.
|
||||
uint32_t file_change_wait_duration_ms = 0;
|
||||
};
|
||||
|
||||
} // namespace cdc_ft
|
||||
|
||||
#endif // CDC_STREAM_SESSION_CONFIG_H_
|
||||
75
cdc_stream/session_management_server.cc
Normal file
75
cdc_stream/session_management_server.cc
Normal file
@@ -0,0 +1,75 @@
|
||||
// Copyright 2022 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "cdc_stream/session_management_server.h"
|
||||
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "cdc_stream/background_service_impl.h"
|
||||
#include "cdc_stream/local_assets_stream_manager_service_impl.h"
|
||||
#include "cdc_stream/session_manager.h"
|
||||
#include "common/log.h"
|
||||
#include "common/status.h"
|
||||
#include "common/status_macros.h"
|
||||
#include "grpcpp/grpcpp.h"
|
||||
|
||||
namespace cdc_ft {
|
||||
|
||||
SessionManagementServer::SessionManagementServer(
|
||||
grpc::Service* session_service, grpc::Service* background_service,
|
||||
SessionManager* session_manager)
|
||||
: session_service_(session_service),
|
||||
background_service_(background_service),
|
||||
session_manager_(session_manager) {}
|
||||
|
||||
SessionManagementServer::~SessionManagementServer() = default;
|
||||
|
||||
absl::Status SessionManagementServer::Start(int port) {
|
||||
assert(!server_);
|
||||
|
||||
// Use 127.0.0.1 here to enforce IPv4. Otherwise, if only IPv4 is blocked on
|
||||
// |port|, the server is started with IPv6 only, but clients are connecting
|
||||
// with IPv4.
|
||||
int selected_port = 0;
|
||||
std::string server_address = absl::StrFormat("127.0.0.1:%i", port);
|
||||
grpc::ServerBuilder builder;
|
||||
builder.AddListeningPort(server_address, grpc::InsecureServerCredentials(),
|
||||
&selected_port);
|
||||
builder.RegisterService(session_service_);
|
||||
builder.RegisterService(background_service_);
|
||||
server_ = builder.BuildAndStart();
|
||||
if (selected_port != port) {
|
||||
return MakeStatus(
|
||||
"Failed to start session management server: Could not listen on port "
|
||||
"%i. Is the port in use?",
|
||||
port);
|
||||
}
|
||||
if (!server_) {
|
||||
return MakeStatus(
|
||||
"Failed to start session management server. Check cdc_stream logs.");
|
||||
}
|
||||
LOG_INFO("Session management server listening on '%s'", server_address);
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
void SessionManagementServer::RunUntilShutdown() { server_->Wait(); }
|
||||
|
||||
absl::Status SessionManagementServer::Shutdown() {
|
||||
assert(server_);
|
||||
RETURN_IF_ERROR(session_manager_->Shutdown(),
|
||||
"Failed to shut down session manager");
|
||||
server_->Shutdown();
|
||||
server_->Wait();
|
||||
}
|
||||
|
||||
} // namespace cdc_ft
|
||||
65
cdc_stream/session_management_server.h
Normal file
65
cdc_stream/session_management_server.h
Normal file
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2022 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef CDC_STREAM_SESSION_MANAGEMENT_SERVER_H_
|
||||
#define CDC_STREAM_SESSION_MANAGEMENT_SERVER_H_
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
|
||||
namespace grpc {
|
||||
class Server;
|
||||
class Service;
|
||||
} // namespace grpc
|
||||
|
||||
namespace cdc_ft {
|
||||
|
||||
class SessionManager;
|
||||
class ProcessFactory;
|
||||
|
||||
// gRPC server for managing streaming sessions. Contains these services:
|
||||
// - LocalAssetsStreamManager
|
||||
// - Background
|
||||
class SessionManagementServer {
|
||||
public:
|
||||
static constexpr int kDefaultServicePort = 44432;
|
||||
|
||||
SessionManagementServer(grpc::Service* session_service,
|
||||
grpc::Service* background_service,
|
||||
SessionManager* session_manager);
|
||||
~SessionManagementServer();
|
||||
|
||||
// Starts the server on the local port |port|.
|
||||
absl::Status Start(int port);
|
||||
|
||||
// Waits until ProcessManager issues an Exit() request to the background
|
||||
// service.
|
||||
void RunUntilShutdown();
|
||||
|
||||
// Shuts down the session manager and the server.
|
||||
absl::Status Shutdown();
|
||||
|
||||
private:
|
||||
grpc::Service* session_service_;
|
||||
grpc::Service* background_service_;
|
||||
SessionManager* const session_manager_;
|
||||
std::unique_ptr<grpc::Server> server_;
|
||||
};
|
||||
|
||||
} // namespace cdc_ft
|
||||
|
||||
#endif // CDC_STREAM_SESSION_MANAGEMENT_SERVER_H_
|
||||
193
cdc_stream/session_manager.cc
Normal file
193
cdc_stream/session_manager.cc
Normal file
@@ -0,0 +1,193 @@
|
||||
// Copyright 2022 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "cdc_stream/session_manager.h"
|
||||
|
||||
#include "absl/strings/str_split.h"
|
||||
#include "cdc_stream/multi_session.h"
|
||||
#include "common/log.h"
|
||||
#include "common/process.h"
|
||||
#include "common/status.h"
|
||||
#include "common/status_macros.h"
|
||||
#include "manifest/manifest_updater.h"
|
||||
|
||||
namespace cdc_ft {
|
||||
namespace {
|
||||
|
||||
// Returns a key to uniquely map a streaming directory |src_dir| to a
|
||||
// MultiSession instance.
|
||||
std::string GetMultiSessionKey(const std::string src_dir) {
|
||||
// Use the cache dir as a key to identify MultiSessions. That way, different
|
||||
// representations of the same dir (e.g. dir and dir\) map to the same
|
||||
// MultiSession.
|
||||
return MultiSession::GetCacheDir(src_dir);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SessionManager::SessionManager(SessionConfig cfg,
|
||||
ProcessFactory* process_factory,
|
||||
metrics::MetricsService* metrics_service)
|
||||
: cfg_(cfg),
|
||||
process_factory_(process_factory),
|
||||
metrics_service_(metrics_service) {}
|
||||
|
||||
SessionManager::~SessionManager() = default;
|
||||
|
||||
absl::Status SessionManager::Shutdown() {
|
||||
absl::MutexLock lock(&sessions_mutex_);
|
||||
|
||||
for (const auto& [key, ms] : sessions_) {
|
||||
LOG_INFO("Shutting down MultiSession for path '%s'", ms->src_dir());
|
||||
RETURN_IF_ERROR(ms->Shutdown(),
|
||||
"Failed to shut down MultiSession for path '%s'",
|
||||
ms->src_dir());
|
||||
}
|
||||
sessions_.clear();
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status SessionManager::StartSession(
|
||||
const std::string& instance_id, const std::string& src_dir,
|
||||
const SessionTarget& target, const std::string& project_id,
|
||||
const std::string& organization_id, MultiSession** multi_session,
|
||||
metrics::SessionStartStatus* metrics_status) {
|
||||
*multi_session = nullptr;
|
||||
*metrics_status = metrics::SessionStartStatus::kOk;
|
||||
|
||||
absl::MutexLock lock(&sessions_mutex_);
|
||||
|
||||
// Check if the directory is correct as early as possible.
|
||||
absl::Status status = ManifestUpdater::IsValidDir(src_dir);
|
||||
if (!status.ok()) {
|
||||
absl::Status stop_status = StopSessionInternal(instance_id);
|
||||
if (!stop_status.ok() && !absl::IsNotFound(stop_status)) {
|
||||
LOG_ERROR("Failed to stop previous session for instance '%s': '%s'",
|
||||
instance_id, stop_status.ToString());
|
||||
}
|
||||
*metrics_status = metrics::SessionStartStatus::kInvalidDirError;
|
||||
return WrapStatus(status, "Failed to start session for path '%s'", src_dir);
|
||||
}
|
||||
|
||||
// Early out if we are streaming the workstation dir to the given gamelet.
|
||||
MultiSession* ms = GetMultiSession(src_dir);
|
||||
*multi_session = ms;
|
||||
if (ms && ms->HasSession(instance_id)) {
|
||||
if (ms->IsSessionHealthy(instance_id)) {
|
||||
LOG_INFO("Reusing existing session");
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
LOG_INFO("Existing session for instance '%s' is not healthy. Restarting.",
|
||||
instance_id);
|
||||
|
||||
// We could also fall through, but this might restart the MultiSession.
|
||||
status = ms->StopSession(instance_id);
|
||||
if (status.ok()) {
|
||||
status =
|
||||
ms->StartSession(instance_id, target, project_id, organization_id);
|
||||
}
|
||||
if (!status.ok()) {
|
||||
*metrics_status = metrics::SessionStartStatus::kRestartSessionError;
|
||||
}
|
||||
return WrapStatus(status, "Failed to restart session for instance '%s'",
|
||||
instance_id);
|
||||
}
|
||||
|
||||
// If we are already streaming to the given gamelet, but from another
|
||||
// workstation directory, stop that session.
|
||||
// Note that NotFoundError is OK and expected (it means no session exists).
|
||||
status = StopSessionInternal(instance_id);
|
||||
if (!status.ok() && !absl::IsNotFound(status)) {
|
||||
*metrics_status = metrics::SessionStartStatus::kStopSessionError;
|
||||
return WrapStatus(status,
|
||||
"Failed to stop previous session for instance '%s'",
|
||||
instance_id);
|
||||
}
|
||||
|
||||
// Get or create the MultiSession for the given workstation directory.
|
||||
absl::StatusOr<MultiSession*> ms_res = GetOrCreateMultiSession(src_dir);
|
||||
if (!ms_res.ok()) {
|
||||
*metrics_status = metrics::SessionStartStatus::kCreateMultiSessionError;
|
||||
return WrapStatus(ms_res.status(),
|
||||
"Failed to create MultiSession for path '%s'", src_dir);
|
||||
}
|
||||
ms = ms_res.value();
|
||||
*multi_session = ms;
|
||||
|
||||
// Start the session.
|
||||
LOG_INFO("Starting streaming session from path '%s' to instance '%s'",
|
||||
src_dir, instance_id);
|
||||
status = ms->StartSession(instance_id, target, project_id, organization_id);
|
||||
if (!status.ok()) {
|
||||
*metrics_status = metrics::SessionStartStatus::kStartSessionError;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
absl::Status SessionManager::StopSession(const std::string& instance_id) {
|
||||
absl::MutexLock lock(&sessions_mutex_);
|
||||
return StopSessionInternal(instance_id);
|
||||
}
|
||||
|
||||
MultiSession* SessionManager::GetMultiSession(const std::string& src_dir) {
|
||||
const std::string key = GetMultiSessionKey(src_dir);
|
||||
SessionMap::iterator iter = sessions_.find(key);
|
||||
return iter != sessions_.end() ? iter->second.get() : nullptr;
|
||||
}
|
||||
|
||||
absl::StatusOr<MultiSession*> SessionManager::GetOrCreateMultiSession(
|
||||
const std::string& src_dir) {
|
||||
const std::string key = GetMultiSessionKey(src_dir);
|
||||
SessionMap::iterator iter = sessions_.find(key);
|
||||
if (iter == sessions_.end()) {
|
||||
LOG_INFO("Creating new MultiSession for path '%s'", src_dir);
|
||||
auto ms = std::make_unique<MultiSession>(
|
||||
src_dir, cfg_, process_factory_,
|
||||
new MultiSessionMetricsRecorder(metrics_service_));
|
||||
RETURN_IF_ERROR(ms->Initialize(), "Failed to initialize MultiSession");
|
||||
iter = sessions_.insert({key, std::move(ms)}).first;
|
||||
}
|
||||
|
||||
return iter->second.get();
|
||||
}
|
||||
|
||||
absl::Status SessionManager::StopSessionInternal(
|
||||
const std::string& instance_id) {
|
||||
absl::Status status;
|
||||
for (const auto& [key, ms] : sessions_) {
|
||||
if (!ms->HasSession(instance_id)) continue;
|
||||
|
||||
LOG_INFO("Stopping session streaming from '%s' to instance '%s'",
|
||||
ms->src_dir(), instance_id);
|
||||
RETURN_IF_ERROR(ms->StopSession(instance_id),
|
||||
"Failed to stop session for instance '%s'", instance_id);
|
||||
|
||||
// Session was stopped. If the MultiSession is empty now, delete it.
|
||||
if (ms->Empty()) {
|
||||
LOG_INFO("Shutting down MultiSession for path '%s'", ms->src_dir());
|
||||
RETURN_IF_ERROR(ms->Shutdown(),
|
||||
"Failed to shut down MultiSession for path '%s'",
|
||||
ms->src_dir());
|
||||
sessions_.erase(key);
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
return absl::NotFoundError(
|
||||
absl::StrFormat("No session for instance '%s' found", instance_id));
|
||||
}
|
||||
|
||||
} // namespace cdc_ft
|
||||
103
cdc_stream/session_manager.h
Normal file
103
cdc_stream/session_manager.h
Normal 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 CDC_STREAM_SESSION_MANAGER_H_
|
||||
#define CDC_STREAM_SESSION_MANAGER_H_
|
||||
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/synchronization/mutex.h"
|
||||
#include "cdc_stream/session_config.h"
|
||||
#include "metrics/metrics.h"
|
||||
|
||||
namespace cdc_ft {
|
||||
|
||||
class MultiSession;
|
||||
class ProcessFactory;
|
||||
struct SessionTarget;
|
||||
|
||||
// Adds logic around MultiSession to start and stop streaming sessions. Makes
|
||||
// sure that some invariants are maintained, like no two streaming sessions
|
||||
// exist to the same target user@host:dir.
|
||||
class SessionManager {
|
||||
public:
|
||||
SessionManager(SessionConfig cfg, ProcessFactory* process_factory,
|
||||
metrics::MetricsService* metrics_service);
|
||||
~SessionManager();
|
||||
|
||||
// Starts a new session or reuses an existing one.
|
||||
// |instance_id| is a unique id for the remote instance and mount directory,
|
||||
// e.g. user@host:mount_dir.
|
||||
// |src_dir| is the local directory to stream.
|
||||
// |target| identifies the remote target and how to connect to it.
|
||||
// |project_id| is the project that owns the instance. Stadia only.
|
||||
// |organization_id| is organization that contains the instance. Stadia only.
|
||||
// Populates |multi_session| and |metrics_status| on success.
|
||||
absl::Status StartSession(const std::string& instance_id,
|
||||
const std::string& src_dir,
|
||||
const SessionTarget& target,
|
||||
const std::string& project_id,
|
||||
const std::string& organization_id,
|
||||
MultiSession** multi_session,
|
||||
metrics::SessionStartStatus* metrics_status)
|
||||
ABSL_LOCKS_EXCLUDED(sessions_mutex_);
|
||||
|
||||
// Stops the session for the given |instance_id|.
|
||||
// Returns a NotFound error if no session exists.
|
||||
absl::Status StopSession(const std::string& instance_id)
|
||||
ABSL_LOCKS_EXCLUDED(sessions_mutex_);
|
||||
|
||||
// Shuts down all existing MultiSessions.
|
||||
absl::Status Shutdown() ABSL_LOCKS_EXCLUDED(sessions_mutex_);
|
||||
|
||||
private:
|
||||
// Stops the session for the given |instance_id|. Returns a NotFound error if
|
||||
// no session exists.
|
||||
absl::Status StopSessionInternal(const std::string& instance_id)
|
||||
ABSL_EXCLUSIVE_LOCKS_REQUIRED(sessions_mutex_);
|
||||
|
||||
// Returns the MultiSession for the given workstation directory |src_dir| or
|
||||
// nullptr if it does not exist.
|
||||
MultiSession* GetMultiSession(const std::string& src_dir)
|
||||
ABSL_EXCLUSIVE_LOCKS_REQUIRED(sessions_mutex_);
|
||||
|
||||
// Gets an existing MultiSession or creates a new one for the given
|
||||
// workstation directory |src_dir|.
|
||||
absl::StatusOr<MultiSession*> GetOrCreateMultiSession(
|
||||
const std::string& src_dir)
|
||||
ABSL_EXCLUSIVE_LOCKS_REQUIRED(sessions_mutex_);
|
||||
|
||||
// Sets session start status for a metrics event.
|
||||
void SetSessionStartStatus(metrics::DeveloperLogEvent* evt,
|
||||
absl::Status absl_status,
|
||||
metrics::SessionStartStatus status) const;
|
||||
|
||||
const SessionConfig cfg_;
|
||||
ProcessFactory* const process_factory_;
|
||||
metrics::MetricsService* const metrics_service_;
|
||||
|
||||
absl::Mutex sessions_mutex_;
|
||||
using SessionMap =
|
||||
std::unordered_map<std::string, std::unique_ptr<MultiSession>>;
|
||||
SessionMap sessions_ ABSL_GUARDED_BY(sessions_mutex_);
|
||||
};
|
||||
|
||||
} // namespace cdc_ft
|
||||
|
||||
#endif // CDC_STREAM_SESSION_MANAGER_H_
|
||||
110
cdc_stream/start_command.cc
Normal file
110
cdc_stream/start_command.cc
Normal file
@@ -0,0 +1,110 @@
|
||||
// Copyright 2022 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "cdc_stream/start_command.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "cdc_stream/local_assets_stream_manager_client.h"
|
||||
#include "cdc_stream/session_management_server.h"
|
||||
#include "common/log.h"
|
||||
#include "common/path.h"
|
||||
#include "common/remote_util.h"
|
||||
#include "common/status_macros.h"
|
||||
#include "lyra/lyra.hpp"
|
||||
|
||||
namespace cdc_ft {
|
||||
namespace {
|
||||
constexpr int kDefaultVerbosity = 2;
|
||||
} // namespace
|
||||
|
||||
StartCommand::StartCommand(int* exit_code)
|
||||
: BaseCommand("start",
|
||||
"Start streaming files from a Windows to a Linux device",
|
||||
exit_code) {}
|
||||
|
||||
StartCommand::~StartCommand() = default;
|
||||
|
||||
void StartCommand::RegisterCommandLineFlags(lyra::command& cmd) {
|
||||
verbosity_ = kDefaultVerbosity;
|
||||
cmd.add_argument(lyra::opt(verbosity_, "num")
|
||||
.name("--verbosity")
|
||||
.help("Verbosity of the log output, default: " +
|
||||
std::to_string(kDefaultVerbosity) +
|
||||
". Increase to make logs more verbose."));
|
||||
|
||||
service_port_ = SessionManagementServer::kDefaultServicePort;
|
||||
cmd.add_argument(
|
||||
lyra::opt(service_port_, "port")
|
||||
.name("--service-port")
|
||||
.help("Local port to use while connecting to the local "
|
||||
"asset stream service, default: " +
|
||||
std::to_string(SessionManagementServer::kDefaultServicePort)));
|
||||
|
||||
ssh_port_ = RemoteUtil::kDefaultSshPort;
|
||||
cmd.add_argument(
|
||||
lyra::opt(ssh_port_, "port")
|
||||
.name("--ssh-port")
|
||||
.help("Port to use while connecting to the remote instance being "
|
||||
"streamed to, default: " +
|
||||
std::to_string(RemoteUtil::kDefaultSshPort)));
|
||||
|
||||
path::GetEnv("CDC_SSH_COMMAND", &ssh_command_).IgnoreError();
|
||||
cmd.add_argument(
|
||||
lyra::opt(ssh_command_, "ssh_command")
|
||||
.name("--ssh-command")
|
||||
.help("Path and arguments of ssh command to use, e.g. "
|
||||
"\"C:\\path\\to\\ssh.exe -F config_file\". Can also be "
|
||||
"specified by the CDC_SSH_COMMAND environment variable."));
|
||||
|
||||
path::GetEnv("CDC_SCP_COMMAND", &scp_command_).IgnoreError();
|
||||
cmd.add_argument(
|
||||
lyra::opt(scp_command_, "scp_command")
|
||||
.name("--scp-command")
|
||||
.help("Path and arguments of scp command to use, e.g. "
|
||||
"\"C:\\path\\to\\scp.exe -F config_file\". Can also be "
|
||||
"specified by the CDC_SCP_COMMAND environment variable."));
|
||||
|
||||
cmd.add_argument(lyra::arg(PosArgValidator(&src_dir_), "dir")
|
||||
.required()
|
||||
.help("Windows directory to stream"));
|
||||
|
||||
cmd.add_argument(
|
||||
lyra::arg(PosArgValidator(&user_host_dir_), "[user@]host:src-dir")
|
||||
.required()
|
||||
.help("Linux host and directory to stream to"));
|
||||
}
|
||||
|
||||
absl::Status StartCommand::Run() {
|
||||
LogLevel level = Log::VerbosityToLogLevel(verbosity_);
|
||||
ScopedLog scoped_log(std::make_unique<ConsoleLog>(level));
|
||||
LocalAssetsStreamManagerClient client(service_port_);
|
||||
|
||||
std::string full_src_dir = path::GetFullPath(src_dir_);
|
||||
std::string user_host, mount_dir;
|
||||
RETURN_IF_ERROR(LocalAssetsStreamManagerClient::ParseUserHostDir(
|
||||
user_host_dir_, &user_host, &mount_dir));
|
||||
|
||||
absl::Status status =
|
||||
client.StartSession(full_src_dir, user_host, ssh_port_, mount_dir,
|
||||
ssh_command_, scp_command_);
|
||||
if (status.ok()) {
|
||||
LOG_INFO("Started streaming directory '%s' to '%s:%s'", src_dir_, user_host,
|
||||
mount_dir);
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
} // namespace cdc_ft
|
||||
48
cdc_stream/start_command.h
Normal file
48
cdc_stream/start_command.h
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2022 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef CDC_STREAM_START_COMMAND_H_
|
||||
#define CDC_STREAM_START_COMMAND_H_
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "cdc_stream/base_command.h"
|
||||
|
||||
namespace cdc_ft {
|
||||
|
||||
// Handler for the start command. Sends an RPC call to the service to starts a
|
||||
// new asset streaming session.
|
||||
class StartCommand : public BaseCommand {
|
||||
public:
|
||||
explicit StartCommand(int* exit_code);
|
||||
~StartCommand();
|
||||
|
||||
// BaseCommand:
|
||||
void RegisterCommandLineFlags(lyra::command& cmd) override;
|
||||
absl::Status Run() override;
|
||||
|
||||
private:
|
||||
int verbosity_ = 0;
|
||||
uint16_t service_port_ = 0;
|
||||
uint16_t ssh_port_ = 0;
|
||||
std::string ssh_command_;
|
||||
std::string scp_command_;
|
||||
std::string src_dir_;
|
||||
std::string user_host_dir_;
|
||||
};
|
||||
|
||||
} // namespace cdc_ft
|
||||
|
||||
#endif // CDC_STREAM_START_COMMAND_H_
|
||||
156
cdc_stream/start_service_command.cc
Normal file
156
cdc_stream/start_service_command.cc
Normal file
@@ -0,0 +1,156 @@
|
||||
// Copyright 2022 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "cdc_stream/start_service_command.h"
|
||||
|
||||
#include "cdc_stream/background_service_impl.h"
|
||||
#include "cdc_stream/local_assets_stream_manager_service_impl.h"
|
||||
#include "cdc_stream/session_management_server.h"
|
||||
#include "cdc_stream/session_manager.h"
|
||||
#include "common/clock.h"
|
||||
#include "common/grpc_status.h"
|
||||
#include "common/log.h"
|
||||
#include "common/path.h"
|
||||
#include "common/process.h"
|
||||
#include "common/status_macros.h"
|
||||
#include "lyra/lyra.hpp"
|
||||
#include "metrics/metrics.h"
|
||||
|
||||
namespace cdc_ft {
|
||||
namespace {
|
||||
|
||||
std::string GetLogPath(const char* log_dir, const char* log_base_name) {
|
||||
DefaultSystemClock* clock = DefaultSystemClock::GetInstance();
|
||||
std::string timestamp_ext = clock->FormatNow(".%Y%m%d-%H%M%S.log", false);
|
||||
return path::Join(log_dir, log_base_name + timestamp_ext);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
StartServiceCommand::StartServiceCommand(int* exit_code)
|
||||
: BaseCommand("start-service", "Start streaming service", exit_code) {}
|
||||
StartServiceCommand::~StartServiceCommand() = default;
|
||||
|
||||
void StartServiceCommand::RegisterCommandLineFlags(lyra::command& cmd) {
|
||||
config_file_ = "%APPDATA%\\cdc-file-transfer\\assets_stream_manager.json";
|
||||
cmd.add_argument(
|
||||
lyra::opt(config_file_, "path")
|
||||
.name("--config-file")
|
||||
.help("Json configuration file, default: " + config_file_));
|
||||
|
||||
log_dir_ = "%APPDATA%\\cdc-file-transfer\\logs";
|
||||
cmd.add_argument(
|
||||
lyra::opt(log_dir_, "dir")
|
||||
.name("--log-dir")
|
||||
.help("Directory to store log files, default: " + log_dir_));
|
||||
|
||||
cfg_.RegisterCommandLineFlags(cmd, *this);
|
||||
}
|
||||
|
||||
absl::Status StartServiceCommand::Run() {
|
||||
// Set up config. Allow overriding this config with |config_file|.
|
||||
absl::Status cfg_load_status = path::ExpandPathVariables(&config_file_);
|
||||
cfg_load_status.Update(cfg_.LoadFromFile(config_file_));
|
||||
|
||||
std::unique_ptr<Log> logger;
|
||||
ASSIGN_OR_RETURN(logger, GetLogger());
|
||||
cdc_ft::ScopedLog scoped_log(std::move(logger));
|
||||
|
||||
// Log status of loaded configuration. Errors are not critical.
|
||||
if (cfg_load_status.ok()) {
|
||||
LOG_INFO("Successfully loaded configuration file at '%s'", config_file_);
|
||||
} else if (absl::IsNotFound(cfg_load_status)) {
|
||||
LOG_INFO("No configuration file found at '%s'", config_file_);
|
||||
} else {
|
||||
LOG_ERROR("%s", cfg_load_status.message());
|
||||
}
|
||||
|
||||
std::string flags_read = cfg_.GetFlagsReadFromFile();
|
||||
if (!flags_read.empty()) {
|
||||
LOG_INFO(
|
||||
"The following settings were read from the configuration file and "
|
||||
"override the corresponding command line flags if set: %s",
|
||||
flags_read);
|
||||
}
|
||||
|
||||
std::string flag_errors = cfg_.GetFlagReadErrors();
|
||||
if (!flag_errors.empty()) {
|
||||
LOG_WARNING("%s", flag_errors);
|
||||
}
|
||||
|
||||
LOG_DEBUG("Configuration:\n%s", cfg_.ToString());
|
||||
|
||||
absl::Status status = RunService();
|
||||
if (!status.ok()) {
|
||||
LOG_ERROR("%s", status.ToString());
|
||||
} else {
|
||||
LOG_INFO("Streaming service shut down successfully.");
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
absl::StatusOr<std::unique_ptr<Log>> StartServiceCommand::GetLogger() {
|
||||
LogLevel level = Log::VerbosityToLogLevel(cfg_.session_cfg().verbosity);
|
||||
if (cfg_.log_to_stdout()) {
|
||||
// Log to stdout.
|
||||
return std::make_unique<ConsoleLog>(level);
|
||||
}
|
||||
|
||||
// Log to file.
|
||||
if (!path::ExpandPathVariables(&log_dir_).ok() ||
|
||||
!path::CreateDirRec(log_dir_).ok()) {
|
||||
return absl::InvalidArgumentError(
|
||||
absl::StrFormat("Failed to create log directory '%s'", log_dir_));
|
||||
}
|
||||
|
||||
return std::make_unique<FileLog>(
|
||||
level, GetLogPath(log_dir_.c_str(), "assets_stream_manager").c_str());
|
||||
}
|
||||
|
||||
// Runs the session management service and returns when it finishes.
|
||||
absl::Status StartServiceCommand::RunService() {
|
||||
WinProcessFactory process_factory;
|
||||
metrics::MetricsService metrics_service;
|
||||
|
||||
SessionManager session_manager(cfg_.session_cfg(), &process_factory,
|
||||
&metrics_service);
|
||||
BackgroundServiceImpl background_service;
|
||||
LocalAssetsStreamManagerServiceImpl session_service(
|
||||
&session_manager, &process_factory, &metrics_service);
|
||||
|
||||
SessionManagementServer sm_server(&session_service, &background_service,
|
||||
&session_manager);
|
||||
background_service.SetExitCallback(
|
||||
[&sm_server]() { return sm_server.Shutdown(); });
|
||||
|
||||
if (!cfg_.dev_src_dir().empty()) {
|
||||
localassetsstreammanager::StartSessionRequest request;
|
||||
request.set_workstation_directory(cfg_.dev_src_dir());
|
||||
request.set_user_host(cfg_.dev_target().user_host);
|
||||
request.set_mount_dir(cfg_.dev_target().mount_dir);
|
||||
request.set_port(cfg_.dev_target().ssh_port);
|
||||
request.set_ssh_command(cfg_.dev_target().ssh_command);
|
||||
request.set_scp_command(cfg_.dev_target().scp_command);
|
||||
localassetsstreammanager::StartSessionResponse response;
|
||||
RETURN_ABSL_IF_ERROR(
|
||||
session_service.StartSession(nullptr, &request, &response));
|
||||
}
|
||||
RETURN_IF_ERROR(
|
||||
sm_server.Start(SessionManagementServer::kDefaultServicePort));
|
||||
sm_server.RunUntilShutdown();
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
} // namespace cdc_ft
|
||||
54
cdc_stream/start_service_command.h
Normal file
54
cdc_stream/start_service_command.h
Normal 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 CDC_STREAM_START_SERVICE_COMMAND_H_
|
||||
#define CDC_STREAM_START_SERVICE_COMMAND_H_
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "cdc_stream/asset_stream_config.h"
|
||||
#include "cdc_stream/base_command.h"
|
||||
|
||||
namespace cdc_ft {
|
||||
|
||||
// Handler for the start-service command. Starts the asset streaming service
|
||||
// and returns when the service is shut down.
|
||||
class StartServiceCommand : public BaseCommand {
|
||||
public:
|
||||
explicit StartServiceCommand(int* exit_code);
|
||||
~StartServiceCommand();
|
||||
|
||||
// BaseCommand:
|
||||
void RegisterCommandLineFlags(lyra::command& cmd) override;
|
||||
absl::Status Run() override;
|
||||
|
||||
private:
|
||||
// Depending on the flags, returns a console or file logger.
|
||||
absl::StatusOr<std::unique_ptr<Log>> GetLogger();
|
||||
|
||||
// Runs the asset streaming service.
|
||||
absl::Status RunService();
|
||||
|
||||
AssetStreamConfig cfg_;
|
||||
std::string config_file_;
|
||||
std::string log_dir_;
|
||||
};
|
||||
|
||||
} // namespace cdc_ft
|
||||
|
||||
#endif // CDC_STREAM_START_SERVICE_COMMAND_H_
|
||||
75
cdc_stream/stop_command.cc
Normal file
75
cdc_stream/stop_command.cc
Normal file
@@ -0,0 +1,75 @@
|
||||
// Copyright 2022 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "cdc_stream/stop_command.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "cdc_stream/local_assets_stream_manager_client.h"
|
||||
#include "cdc_stream/session_management_server.h"
|
||||
#include "common/log.h"
|
||||
#include "common/path.h"
|
||||
#include "common/status_macros.h"
|
||||
#include "lyra/lyra.hpp"
|
||||
|
||||
namespace cdc_ft {
|
||||
namespace {
|
||||
constexpr int kDefaultVerbosity = 2;
|
||||
} // namespace
|
||||
|
||||
StopCommand::StopCommand(int* exit_code)
|
||||
: BaseCommand("stop", "Stops a streaming session", exit_code) {}
|
||||
|
||||
StopCommand::~StopCommand() = default;
|
||||
|
||||
void StopCommand::RegisterCommandLineFlags(lyra::command& cmd) {
|
||||
verbosity_ = kDefaultVerbosity;
|
||||
cmd.add_argument(lyra::opt(verbosity_, "num")
|
||||
.name("--verbosity")
|
||||
.help("Verbosity of the log output, default: " +
|
||||
std::to_string(kDefaultVerbosity) +
|
||||
". Increase to make logs more verbose."));
|
||||
|
||||
service_port_ = SessionManagementServer::kDefaultServicePort;
|
||||
cmd.add_argument(
|
||||
lyra::opt(service_port_, "port")
|
||||
.name("--service-port")
|
||||
.help("Local port to use while connecting to the local "
|
||||
"asset stream service, default: " +
|
||||
std::to_string(SessionManagementServer::kDefaultServicePort)));
|
||||
|
||||
cmd.add_argument(
|
||||
lyra::arg(PosArgValidator(&user_host_dir_), "[user@]host:src-dir")
|
||||
.required()
|
||||
.help("Linux host and directory to stream to"));
|
||||
}
|
||||
|
||||
absl::Status StopCommand::Run() {
|
||||
LogLevel level = Log::VerbosityToLogLevel(verbosity_);
|
||||
ScopedLog scoped_log(std::make_unique<ConsoleLog>(level));
|
||||
LocalAssetsStreamManagerClient client(service_port_);
|
||||
|
||||
std::string user_host, mount_dir;
|
||||
RETURN_IF_ERROR(LocalAssetsStreamManagerClient::ParseUserHostDir(
|
||||
user_host_dir_, &user_host, &mount_dir));
|
||||
|
||||
absl::Status status = client.StopSession(user_host, mount_dir);
|
||||
if (status.ok()) {
|
||||
LOG_INFO("Stopped streaming session to '%s:%s'", user_host, mount_dir);
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
} // namespace cdc_ft
|
||||
44
cdc_stream/stop_command.h
Normal file
44
cdc_stream/stop_command.h
Normal 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.
|
||||
*/
|
||||
|
||||
#ifndef CDC_STREAM_STOP_COMMAND_H_
|
||||
#define CDC_STREAM_STOP_COMMAND_H_
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "cdc_stream/base_command.h"
|
||||
|
||||
namespace cdc_ft {
|
||||
|
||||
// Handler for the stop command. Sends an RPC call to the service to stop an
|
||||
// asset streaming session.
|
||||
class StopCommand : public BaseCommand {
|
||||
public:
|
||||
explicit StopCommand(int* exit_code);
|
||||
~StopCommand();
|
||||
|
||||
// BaseCommand:
|
||||
void RegisterCommandLineFlags(lyra::command& cmd) override;
|
||||
absl::Status Run() override;
|
||||
|
||||
private:
|
||||
int verbosity_ = 0;
|
||||
uint16_t service_port_ = 0;
|
||||
std::string user_host_dir_;
|
||||
};
|
||||
|
||||
} // namespace cdc_ft
|
||||
|
||||
#endif // CDC_STREAM_STOP_COMMAND_H_
|
||||
1
cdc_stream/testdata/multi_session/non_empty/a.txt
vendored
Normal file
1
cdc_stream/testdata/multi_session/non_empty/a.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
aaaaaaaa
|
||||
1
cdc_stream/testdata/multi_session/non_empty/subdir/b.txt
vendored
Normal file
1
cdc_stream/testdata/multi_session/non_empty/subdir/b.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
|
||||
1
cdc_stream/testdata/multi_session/non_empty/subdir/c.txt
vendored
Normal file
1
cdc_stream/testdata/multi_session/non_empty/subdir/c.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
c
|
||||
1
cdc_stream/testdata/multi_session/non_empty/subdir/d.txt
vendored
Normal file
1
cdc_stream/testdata/multi_session/non_empty/subdir/d.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
d
|
||||
0
cdc_stream/testdata/root.txt
vendored
Normal file
0
cdc_stream/testdata/root.txt
vendored
Normal file
50
cdc_stream/testing_asset_stream_server.cc
Normal file
50
cdc_stream/testing_asset_stream_server.cc
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright 2022 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "cdc_stream/testing_asset_stream_server.h"
|
||||
|
||||
#include "data_store/data_store_reader.h"
|
||||
#include "manifest/file_chunk_map.h"
|
||||
|
||||
namespace cdc_ft {
|
||||
|
||||
TestingAssetStreamServer::TestingAssetStreamServer(
|
||||
std::string src_dir, DataStoreReader* data_store_reader,
|
||||
FileChunkMap* file_chunks)
|
||||
: AssetStreamServer(src_dir, data_store_reader, file_chunks) {}
|
||||
|
||||
TestingAssetStreamServer::~TestingAssetStreamServer() = default;
|
||||
|
||||
absl::Status TestingAssetStreamServer::Start(int port) {
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
void TestingAssetStreamServer::SetManifestId(
|
||||
const ContentIdProto& manifest_id) {
|
||||
absl::MutexLock lock(&mutex_);
|
||||
manifest_id_ = manifest_id;
|
||||
}
|
||||
|
||||
absl::Status TestingAssetStreamServer::WaitForManifestAck(
|
||||
const std::string& instance, absl::Duration timeout) {
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
void TestingAssetStreamServer::Shutdown() {}
|
||||
|
||||
ContentIdProto TestingAssetStreamServer::GetManifestId() const {
|
||||
absl::MutexLock lock(&mutex_);
|
||||
return manifest_id_;
|
||||
}
|
||||
} // namespace cdc_ft
|
||||
60
cdc_stream/testing_asset_stream_server.h
Normal file
60
cdc_stream/testing_asset_stream_server.h
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2022 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef CDC_STREAM_TESTING_ASSET_STREAM_SERVER_H_
|
||||
#define CDC_STREAM_TESTING_ASSET_STREAM_SERVER_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "absl/base/thread_annotations.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/synchronization/mutex.h"
|
||||
#include "cdc_stream/grpc_asset_stream_server.h"
|
||||
#include "manifest/manifest_proto_defs.h"
|
||||
|
||||
namespace cdc_ft {
|
||||
|
||||
// Not thread-safe testing server for streaming assets.
|
||||
class TestingAssetStreamServer : public AssetStreamServer {
|
||||
public:
|
||||
TestingAssetStreamServer(std::string src_dir,
|
||||
DataStoreReader* data_store_reader,
|
||||
FileChunkMap* file_chunks);
|
||||
|
||||
~TestingAssetStreamServer();
|
||||
|
||||
// AssetStreamServer:
|
||||
|
||||
absl::Status Start(int port) override;
|
||||
|
||||
void SetManifestId(const ContentIdProto& manifest_id)
|
||||
ABSL_LOCKS_EXCLUDED(mutex_) override;
|
||||
|
||||
absl::Status WaitForManifestAck(const std::string& instance,
|
||||
absl::Duration timeout) override;
|
||||
void Shutdown() override;
|
||||
|
||||
ContentIdProto GetManifestId() const ABSL_LOCKS_EXCLUDED(mutex_) override;
|
||||
|
||||
private:
|
||||
mutable absl::Mutex mutex_;
|
||||
ContentIdProto manifest_id_ ABSL_GUARDED_BY(mutex_);
|
||||
};
|
||||
|
||||
} // namespace cdc_ft
|
||||
|
||||
#endif // CDC_STREAM_TESTING_ASSET_STREAM_SERVER_H_
|
||||
Reference in New Issue
Block a user