[cdc_stream] Add a CLI client to start/stop asset streaming sessions (#4)

Implements the cdc_stream client and adjusts asset streaming in
various places to work better outside of a GGP environment.

This CL tries to get quoting for SSH commands right. It also brings
back the ability to start a streaming session from
asset_stream_manager.

Also cleans up Bazel targets setup. Since the sln file is now in root,
it is no longer necessary to prepend ../ to relative filenames to
make clicking on errors work.
This commit is contained in:
Lutz Justen
2022-11-18 10:59:42 +01:00
committed by GitHub
parent ca84d3dd2e
commit 269fb2be45
38 changed files with 797 additions and 356 deletions

View File

@@ -326,6 +326,10 @@ std::string GetDrivePrefix(const std::string& path) {
std::string GetCwd() { return std::filesystem::current_path().u8string(); }
void SetCwd(const std::string& path) {
std::filesystem::current_path(std::filesystem::u8path(path));
}
std::string GetFullPath(const std::string& path) {
if (path.empty()) {
return std::string();

View File

@@ -130,6 +130,9 @@ std::string GetDrivePrefix(const std::string& path);
// Gets the current working directory.
std::string GetCwd();
// Sets the current working directory.
void SetCwd(const std::string& path);
// Expands a relative path to an absolute path (relative to the current working
// directory). Also canonicalizes the path, removing any . and .. elements.
// Note that if the path does not exist or contains invalid characters, it may

View File

@@ -16,6 +16,7 @@
#include <regex>
#include "absl/strings/match.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "common/path.h"
@@ -46,6 +47,7 @@ void RemoteUtil::SetUserHostAndPort(std::string user_host, int port) {
user_host_ = std::move(user_host);
ssh_port_ = port;
}
void RemoteUtil::SetScpCommand(std::string scp_command) {
scp_command_ = std::move(scp_command);
}
@@ -56,7 +58,7 @@ void RemoteUtil::SetSshCommand(std::string ssh_command) {
absl::Status RemoteUtil::Scp(std::vector<std::string> source_filepaths,
const std::string& dest, bool compress) {
absl::Status status = CheckHostPort();
absl::Status status = CheckUserHostPort();
if (!status.ok()) {
return status;
}
@@ -64,7 +66,11 @@ absl::Status RemoteUtil::Scp(std::vector<std::string> source_filepaths,
std::string source_args;
for (const std::string& sourceFilePath : source_filepaths) {
// Workaround for scp thinking that C is a host in C:\path\to\foo.
source_args += QuoteArgument("//./" + sourceFilePath) + " ";
if (absl::StrContains(path::GetDrivePrefix(sourceFilePath), ":")) {
source_args += QuoteForWindows("//./" + sourceFilePath) + " ";
} else {
source_args += QuoteForWindows(sourceFilePath) + " ";
}
}
// -p preserves timestamps. This enables timestamp-based up-to-date checks.
@@ -73,66 +79,27 @@ absl::Status RemoteUtil::Scp(std::vector<std::string> source_filepaths,
"%s "
"%s %s -p -T "
"-P %i %s "
"%s",
scp_command_, quiet_ || verbosity_ < 2 ? "-q" : "", compress ? "-C" : "",
ssh_port_, source_args, QuoteArgument(user_host_ + ":" + dest));
"%s:%s",
QuoteForWindows(scp_command_), quiet_ || verbosity_ < 2 ? "-q" : "",
compress ? "-C" : "", ssh_port_, source_args, QuoteForWindows(user_host_),
QuoteForWindows(dest));
start_info.name = "scp";
start_info.forward_output_to_log = forward_output_to_log_;
return process_factory_->Run(start_info);
}
absl::Status RemoteUtil::Sync(std::vector<std::string> source_filepaths,
const std::string& dest) {
absl::Status status = CheckHostPort();
if (!status.ok()) {
return status;
}
std::string source_args;
for (const std::string& sourceFilePath : source_filepaths) {
source_args += QuoteArgument(sourceFilePath) + " ";
}
ProcessStartInfo start_info;
start_info.command = absl::StrFormat(
"cdc_rsync --ip=%s --port=%i -z "
"%s %s%s",
QuoteArgument(user_host_), ssh_port_,
quiet_ || verbosity_ < 2 ? "-q " : " ", source_args, QuoteArgument(dest));
start_info.name = "cdc_rsync";
start_info.forward_output_to_log = forward_output_to_log_;
return process_factory_->Run(start_info);
}
absl::Status RemoteUtil::Chmod(const std::string& mode,
const std::string& remote_path, bool quiet) {
std::string remote_command =
absl::StrFormat("chmod %s %s %s", QuoteArgument(mode),
EscapeForWindows(remote_path), quiet ? "-f" : "");
absl::StrFormat("chmod %s %s %s", QuoteForSsh(mode),
QuoteForSsh(remote_path), quiet ? "-f" : "");
return Run(remote_command, "chmod");
}
absl::Status RemoteUtil::Rm(const std::string& remote_path, bool force) {
std::string remote_command = absl::StrFormat("rm %s %s", force ? "-f" : "",
EscapeForWindows(remote_path));
return Run(remote_command, "rm");
}
absl::Status RemoteUtil::Mv(const std::string& old_remote_path,
const std::string& new_remote_path) {
std::string remote_command =
absl::StrFormat("mv %s %s", EscapeForWindows(old_remote_path),
EscapeForWindows(new_remote_path));
return Run(remote_command, "mv");
}
absl::Status RemoteUtil::Run(std::string remote_command, std::string name) {
absl::Status status = CheckHostPort();
absl::Status status = CheckUserHostPort();
if (!status.ok()) {
return status;
}
@@ -177,33 +144,61 @@ ProcessStartInfo RemoteUtil::BuildProcessStartInfoForSshInternal(
"-oServerAliveCountMax=6 " // Number of lost msgs before ssh terminates
"-oServerAliveInterval=5 " // Time interval between alive msgs
"%s %s -p %i %s",
ssh_command_, quiet_ || verbosity_ < 2 ? "-q" : "", forward_arg,
QuoteArgument(user_host_), ssh_port_, remote_command_arg);
QuoteForWindows(ssh_command_), quiet_ || verbosity_ < 2 ? "-q" : "",
forward_arg, QuoteForWindows(user_host_), ssh_port_, remote_command_arg);
start_info.forward_output_to_log = forward_output_to_log_;
return start_info;
}
std::string RemoteUtil::EscapeForWindows(const std::string& argument) {
std::string str =
std::string RemoteUtil::QuoteForWindows(const std::string& argument) {
// Escape certain backslashes (see doc of this function).
std::string escaped =
std::regex_replace(argument, std::regex(R"(\\*(?="|$))"), "$&$&");
return std::regex_replace(str, std::regex(R"(")"), R"(\")");
// Escape " -> \".
escaped = std::regex_replace(escaped, std::regex(R"(")"), R"(\")");
// Quote.
return absl::StrCat("\"", escaped, "\"");
}
std::string RemoteUtil::QuoteArgument(const std::string& argument) {
return absl::StrCat("\"", EscapeForWindows(argument), "\"");
std::string RemoteUtil::QuoteForSsh(const std::string& argument) {
// Escape \ ->: \\.
std::string escaped =
std::regex_replace(argument, std::regex(R"(\\)"), R"(\\)");
// Escape " -> \".
escaped = std::regex_replace(escaped, std::regex(R"(")"), R"(\")");
// Quote, but handle special case for ~.
if (escaped.empty() || escaped[0] != '~') {
return QuoteForWindows(absl::StrCat("\"", escaped, "\""));
}
// Simple special cases. Quote() isn't required, but called for consistency.
if (escaped == "~" || escaped == "~/") {
return QuoteForWindows(escaped);
}
// Check whether the username contains only valid characters.
// E.g. ~user name/foo -> Quote(~user name/foo)
size_t slash_pos = escaped.find('/');
size_t username_end_pos =
slash_pos == std::string::npos ? escaped.size() : slash_pos;
if (username_end_pos > 1 &&
!std::regex_match(escaped.substr(1, username_end_pos - 1),
std::regex("^[a-z][-a-z0-9]*"))) {
return QuoteForWindows(absl::StrCat("\"", escaped, "\""));
}
if (slash_pos == std::string::npos) {
// E.g. ~username -> Quote(~username)
return QuoteForWindows(escaped);
}
// E.g. or ~username/foo -> Quote(~username/"foo")
return QuoteForWindows(absl::StrCat(escaped.substr(0, slash_pos + 1), "\"",
escaped.substr(slash_pos + 1), "\""));
}
std::string RemoteUtil::QuoteArgumentForSsh(const std::string& argument) {
return absl::StrFormat(
"'%s'", std::regex_replace(argument, std::regex("'"), "'\\''"));
}
std::string RemoteUtil::QuoteAndEscapeArgumentForSsh(
const std::string& argument) {
return EscapeForWindows(QuoteArgumentForSsh(argument));
}
absl::Status RemoteUtil::CheckHostPort() {
absl::Status RemoteUtil::CheckUserHostPort() {
if (user_host_.empty() || ssh_port_ == 0) {
return MakeStatus("IP or port not set");
}

View File

@@ -61,25 +61,11 @@ class RemoteUtil {
absl::Status Scp(std::vector<std::string> source_filepaths,
const std::string& dest, bool compress);
// Syncs |source_filepaths| to the remote folder |dest| on the gamelet using
// cdc_rsync. Must call SetUserHostAndPort before calling this method.
absl::Status Sync(std::vector<std::string> source_filepaths,
const std::string& dest);
// Calls 'chmod |mode| |remote_path|' on the gamelet.
// Must call SetUserHostAndPort before calling this method.
absl::Status Chmod(const std::string& mode, const std::string& remote_path,
bool quiet = false);
// Calls 'rm [-f] |remote_path|' on the gamelet.
// Must call SetUserHostAndPort before calling this method.
absl::Status Rm(const std::string& remote_path, bool force);
// Calls `mv |old_remote_path| |new_remote_path| on the gamelet.
// Must call SetUserHostAndPort before calling this method.
absl::Status Mv(const std::string& old_remote_path,
const std::string& new_remote_path);
// Runs |remote_command| on the gamelet. The command must be properly escaped.
// |name| is the name of the command displayed in the logs.
// Must call SetUserHostAndPort before calling this method.
@@ -107,28 +93,32 @@ class RemoteUtil {
// Returns whether output is suppressed.
bool Quiet() const { return quiet_; }
// Escapes command line argument for the Microsoft command line parser in
// preparation for quoting. Double quotes are backslash-escaped. One or more
// backslashes are backslash-escaped if they are followed by a double quote,
// or if they occur at the end of the string, e.g.
// foo\bar -> foo\bar, foo\ -> foo\\, foo\\"bar -> foo\\\\\"bar.
static std::string EscapeForWindows(const std::string& argument);
// Quotes and escapes a command line argument following the convention
// understood by the Microsoft command line parser.
static std::string QuoteArgument(const std::string& argument);
// Quotes and escapes a command line argument for usage in SSH.
static std::string QuoteArgumentForSsh(const std::string& argument);
// Double quotes are backslash-escaped. One or more backslashes are backslash-
// escaped if they are followed by a double quote, or if they occur at the end
// of the string, e.g.
// foo -> "foo"
// foo\bar -> "foo\bar"
// foo\ -> "foo\\"
// foo\\"bar -> "foo\\\\\"bar".
static std::string QuoteForWindows(const std::string& argument);
// Quotes and escapes a command line arguments for use in SSH command. The
// argument is first escaped and quoted for Linux using single quotes and then
// argument is first escaped and quoted for Linux using double quotes and then
// it is escaped to be used by the Microsoft command line parser.
static std::string QuoteAndEscapeArgumentForSsh(const std::string& argument);
// Properly supports path starting with ~ and ~username.
// foo -> "\"foo\""
// foo\bar -> "\"foo\bar\""
// foo\ -> "\"foo\\\\\""
// foo\"bar -> "\"foo\\\\\\\"bar\"".
// ~/foo -> "~/\"foo\""
// ~user/foo -> "~user/\"foo\""
static std::string QuoteForSsh(const std::string& argument);
private:
// Verifies that both || and |ssh_port_| are set.
absl::Status CheckHostPort();
// Verifies that both |user_host_| and |ssh_port_| are set.
absl::Status CheckUserHostPort();
// Common code for BuildProcessStartInfoForSsh*.
ProcessStartInfo BuildProcessStartInfoForSshInternal(

View File

@@ -97,35 +97,40 @@ TEST_F(RemoteUtilTest, BuildProcessStartInfoForSshWithCustomCommand) {
ExpectContains(si.command, {kCustomSshCmd});
}
TEST_F(RemoteUtilTest, EscapeForWindows) {
EXPECT_EQ("foo", RemoteUtil::EscapeForWindows("foo"));
EXPECT_EQ("foo bar", RemoteUtil::EscapeForWindows("foo bar"));
EXPECT_EQ("foo\\bar", RemoteUtil::EscapeForWindows("foo\\bar"));
EXPECT_EQ("\\\\foo", RemoteUtil::EscapeForWindows("\\\\foo"));
EXPECT_EQ("foo\\\\", RemoteUtil::EscapeForWindows("foo\\"));
EXPECT_EQ("foo\\\\\\\\", RemoteUtil::EscapeForWindows("foo\\\\"));
EXPECT_EQ("foo\\\"", RemoteUtil::EscapeForWindows("foo\""));
EXPECT_EQ("foo\\\"bar", RemoteUtil::EscapeForWindows("foo\"bar"));
EXPECT_EQ("foo\\\\\\\"bar", RemoteUtil::EscapeForWindows("foo\\\"bar"));
EXPECT_EQ("foo\\\\\\\\\\\"bar", RemoteUtil::EscapeForWindows("foo\\\\\"bar"));
EXPECT_EQ("\\\"foo\\\"", RemoteUtil::EscapeForWindows("\"foo\""));
EXPECT_EQ("\\\" \\file.txt", RemoteUtil::EscapeForWindows("\" \\file.txt"));
TEST_F(RemoteUtilTest, QuoteForWindows) {
EXPECT_EQ(RemoteUtil::QuoteForWindows("foo"), "\"foo\"");
EXPECT_EQ(RemoteUtil::QuoteForWindows("foo bar"), "\"foo bar\"");
EXPECT_EQ(RemoteUtil::QuoteForWindows("foo\\bar"), "\"foo\\bar\"");
EXPECT_EQ(RemoteUtil::QuoteForWindows("\\\\foo"), "\"\\\\foo\"");
EXPECT_EQ(RemoteUtil::QuoteForWindows("foo\\"), "\"foo\\\\\"");
EXPECT_EQ(RemoteUtil::QuoteForWindows("foo\\\\"), "\"foo\\\\\\\\\"");
EXPECT_EQ(RemoteUtil::QuoteForWindows("foo\""), "\"foo\\\"\"");
EXPECT_EQ(RemoteUtil::QuoteForWindows("foo\"bar"), "\"foo\\\"bar\"");
EXPECT_EQ(RemoteUtil::QuoteForWindows("foo\\\"bar"), "\"foo\\\\\\\"bar\"");
EXPECT_EQ(RemoteUtil::QuoteForWindows("foo\\\\\"bar"),
"\"foo\\\\\\\\\\\"bar\"");
EXPECT_EQ(RemoteUtil::QuoteForWindows("\"foo\""), "\"\\\"foo\\\"\"");
EXPECT_EQ(RemoteUtil::QuoteForWindows("\" \\file.txt"),
"\"\\\" \\file.txt\"");
}
TEST_F(RemoteUtilTest, QuoteArgument) {
EXPECT_EQ("\"foo\"", RemoteUtil::QuoteArgument("foo"));
EXPECT_EQ("\"foo bar\"", RemoteUtil::QuoteArgument("foo bar"));
EXPECT_EQ("\"foo\\bar\"", RemoteUtil::QuoteArgument("foo\\bar"));
EXPECT_EQ("\"\\\\foo\"", RemoteUtil::QuoteArgument("\\\\foo"));
EXPECT_EQ("\"foo\\\\\"", RemoteUtil::QuoteArgument("foo\\"));
EXPECT_EQ("\"foo\\\\\\\\\"", RemoteUtil::QuoteArgument("foo\\\\"));
EXPECT_EQ("\"foo\\\"\"", RemoteUtil::QuoteArgument("foo\""));
EXPECT_EQ("\"foo\\\"bar\"", RemoteUtil::QuoteArgument("foo\"bar"));
EXPECT_EQ("\"foo\\\\\\\"bar\"", RemoteUtil::QuoteArgument("foo\\\"bar"));
EXPECT_EQ("\"foo\\\\\\\\\\\"bar\"",
RemoteUtil::QuoteArgument("foo\\\\\"bar"));
EXPECT_EQ("\"\\\"foo\\\"\"", RemoteUtil::QuoteArgument("\"foo\""));
EXPECT_EQ("\"\\\" \\file.txt\"", RemoteUtil::QuoteArgument("\" \\file.txt"));
TEST_F(RemoteUtilTest, QuoteForSsh) {
EXPECT_EQ(RemoteUtil::QuoteForSsh("foo"), "\"\\\"foo\\\"\"");
EXPECT_EQ(RemoteUtil::QuoteForSsh("foo\\bar"), "\"\\\"foo\\\\bar\\\"\"");
EXPECT_EQ(RemoteUtil::QuoteForSsh("foo\\"), "\"\\\"foo\\\\\\\\\\\"\"");
EXPECT_EQ(RemoteUtil::QuoteForSsh("foo\\\"bar"),
"\"\\\"foo\\\\\\\\\\\\\\\"bar\\\"\"");
EXPECT_EQ(RemoteUtil::QuoteForSsh("~"), "\"~\"");
EXPECT_EQ(RemoteUtil::QuoteForSsh("~username"), "\"~username\"");
EXPECT_EQ(RemoteUtil::QuoteForSsh("~/foo"), "\"~/\\\"foo\\\"\"");
EXPECT_EQ(RemoteUtil::QuoteForSsh("~username/foo"),
"\"~username/\\\"foo\\\"\"");
EXPECT_EQ(RemoteUtil::QuoteForSsh("~invalid user name"),
"\"\\\"~invalid user name\\\"\"");
EXPECT_EQ(RemoteUtil::QuoteForSsh("~invalid user name/foo"),
"\"\\\"~invalid user name/foo\\\"\"");
EXPECT_EQ(RemoteUtil::QuoteForSsh("~user-name69/foo"),
"\"~user-name69/\\\"foo\\\"\""); // Nice!
}
} // namespace