[cdc_stream] Automatically start service (#28)

Starts the streaming service if it's not up and running. This required
adding the ability to run a detached process. By default, all child
processes are killed when the parent process exits. Since detached
child processes don't run with a console, they need to create sub-
processes with CREATE_NO_WINDOW since otherwise a new console pops up,
e.g. for every ssh command.

Polls for 20 seconds while the service starts up. For this purpose,
a BackgroundServiceClient is added. This will be reused in a future CL
by a new stop-service command to exit the service.

Also adds --service-port as additional argument to start-service.
This commit is contained in:
Lutz Justen
2022-12-02 14:34:36 +01:00
committed by GitHub
parent 6d63aa72d7
commit 1120dcbee0
21 changed files with 372 additions and 76 deletions

View File

@@ -213,6 +213,7 @@ absl::StatusOr<std::unordered_set<int>> PortManager::FindAvailableLocalPorts(
ProcessStartInfo start_info;
start_info.command = "netstat -a -n -p tcp";
start_info.name = "netstat";
start_info.flags = ProcessFlags::kNoWindow;
std::string output;
start_info.stdout_handler = [&output](const char* data, size_t data_size) {
@@ -246,6 +247,7 @@ absl::StatusOr<std::unordered_set<int>> PortManager::FindAvailableRemotePorts(
ProcessStartInfo start_info =
remote_util->BuildProcessStartInfoForSsh(remote_command);
start_info.name = "netstat";
start_info.flags = ProcessFlags::kNoWindow;
std::string output;
start_info.stdout_handler = [&output](const char* data, size_t data_size) {

View File

@@ -33,6 +33,12 @@ namespace cdc_ft {
absl::Status LogOutput(const char* name, const char* data, size_t data_size,
absl::optional<LogLevel> log_level = {});
enum class ProcessFlags {
kNone = 0,
kDetached = 1 << 0,
kNoWindow = 1 << 1,
};
struct ProcessStartInfo {
// Handler for stdout/stderr. |data| is guaranteed to be NULL terminated, so
// it may be used like a C-string if it's known to be text, e.g. for printf().
@@ -63,8 +69,14 @@ struct ProcessStartInfo {
OutputHandler stdout_handler;
OutputHandler stderr_handler;
// Flags that define additional properties of the process.
ProcessFlags flags = ProcessFlags::kNone;
// Returns |name| if set, otherwise |command|.
const std::string& Name() const;
// Tests ALL flags (flags & flag) == flag.
bool HasFlag(ProcessFlags flag) const;
};
// Runs a background process and pipes stdin/stdout/stderr.
@@ -75,6 +87,8 @@ class Process {
static constexpr uint32_t kExitCodeFailedToGetExitCode = 4000000002;
explicit Process(const ProcessStartInfo& start_info);
// Terminates the process unless it's running with ProcessFlags::kDetached.
virtual ~Process();
// Start the background process.
@@ -140,6 +154,16 @@ class WinProcessFactory : public ProcessFactory {
std::unique_ptr<Process> Create(const ProcessStartInfo& start_info) override;
};
inline ProcessFlags operator|(ProcessFlags a, ProcessFlags b) {
using T = std::underlying_type_t<ProcessFlags>;
return static_cast<ProcessFlags>(static_cast<T>(a) | static_cast<T>(b));
}
inline ProcessFlags operator&(ProcessFlags a, ProcessFlags b) {
using T = std::underlying_type_t<ProcessFlags>;
return static_cast<ProcessFlags>(static_cast<T>(a) & static_cast<T>(b));
}
} // namespace cdc_ft
#endif // COMMON_PROCESS_H_

View File

@@ -49,6 +49,24 @@ void SetThreadName(const std::string& name) {
}
}
int ToCreationFlags(ProcessFlags pflags) {
#define HANDLE_FLAG(pflag, cflag) \
if ((pflags & pflag) == pflag) { \
cflags |= cflag; \
pdone = pdone | pflag; \
}
int cflags = 0;
ProcessFlags pdone = ProcessFlags::kNone;
HANDLE_FLAG(ProcessFlags::kDetached, DETACHED_PROCESS);
HANDLE_FLAG(ProcessFlags::kNoWindow, CREATE_NO_WINDOW);
assert(pflags == pdone);
#undef HANDLE_FLAG
return cflags;
}
std::atomic_int g_pipe_serial_number{0};
// Creates a pipe suitable for overlapped IO. Regular anonymous pipes in Windows
@@ -567,6 +585,10 @@ const std::string& ProcessStartInfo::Name() const {
return !name.empty() ? name : command;
}
bool ProcessStartInfo::HasFlag(ProcessFlags flag) const {
return (flags & flag) == flag;
}
Process::Process(const ProcessStartInfo& start_info)
: start_info_(start_info) {}
@@ -593,6 +615,8 @@ class WinProcess : public Process {
absl::Status GetStatus() const override;
private:
void Reset();
std::unique_ptr<ProcessInfo> process_info_;
std::unique_ptr<MessagePumpThread> message_pump_;
};
@@ -600,7 +624,14 @@ class WinProcess : public Process {
WinProcess::WinProcess(const ProcessStartInfo& start_info)
: Process(start_info) {}
WinProcess::~WinProcess() { Terminate().IgnoreError(); }
WinProcess::~WinProcess() {
if (start_info_.HasFlag(ProcessFlags::kDetached)) {
// If the process runs detached, just reset handles, don't terminate it.
Reset();
} else {
Terminate().IgnoreError();
}
}
absl::Status WinProcess::Start() {
LOG_INFO("Starting process %s", start_info_.command.c_str());
@@ -676,10 +707,13 @@ absl::Status WinProcess::Start() {
}
JOBOBJECT_EXTENDED_LIMIT_INFORMATION jeli = {0};
jeli.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
if (!start_info_.HasFlag(ProcessFlags::kDetached)) {
jeli.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
}
bool success = SetInformationJobObject(process_info_->job.Get(),
JobObjectExtendedLimitInformation,
&jeli, sizeof(jeli));
if (!success) {
return MakeStatus("SetInformationJobObject() failed: %s",
Util::GetLastWin32Error());
@@ -691,7 +725,7 @@ absl::Status WinProcess::Start() {
NULL, // Process handle not inheritable
NULL, // Thread handle not inheritable
TRUE, // Inherit handles
0, // No creation flags
ToCreationFlags(start_info_.flags),
NULL, // Use parent's environment block
NULL, // Use parent's starting directory
&si, &process_info_->pi);
@@ -785,32 +819,39 @@ absl::Status WinProcess::Terminate() {
message_pump_.reset();
}
if (process_info_) {
bool result = true;
if (should_terminate) {
result = TerminateProcess(process_info_->pi.hProcess, 0);
if (!result && GetLastError() == ERROR_ACCESS_DENIED) {
// This means that the process has already exited, but in a way that
// the exit wasn't properly reported to this code (e.g. the process got
// killed somewhere). Just handle this silently.
LOG_DEBUG("Process '%s' already exited", start_info_.Name());
result = true;
}
std::string error_msg;
if (process_info_ && should_terminate &&
!TerminateProcess(process_info_->pi.hProcess, 0)) {
if (GetLastError() == ERROR_ACCESS_DENIED) {
// This means that the process has already exited, but in a way that
// the exit wasn't properly reported to this code (e.g. the process got
// killed somewhere). Just handle this silently.
LOG_DEBUG("Process '%s' already exited", start_info_.Name());
} else {
error_msg = Util::GetLastWin32Error();
}
}
// Reset handles.
Reset();
if (!error_msg.empty()) {
return MakeStatus("TerminateProcess() failed: %s", error_msg);
}
return absl::OkStatus();
}
void WinProcess::Reset() {
// Shut down message pump.
message_pump_.reset();
if (process_info_) {
// Close the handles that are not scoped handles.
ScopedHandle(process_info_->pi.hProcess).Close();
ScopedHandle(process_info_->pi.hThread).Close();
process_info_.reset();
if (!result) {
return MakeStatus("TerminateProcess() failed: %s",
Util::GetLastWin32Error());
}
}
return absl::OkStatus();
}
ProcessFactory::~ProcessFactory() = default;

View File

@@ -75,6 +75,7 @@ absl::Status RemoteUtil::Scp(std::vector<std::string> source_filepaths,
// -p preserves timestamps. This enables timestamp-based up-to-date checks.
ProcessStartInfo start_info;
start_info.flags = ProcessFlags::kNoWindow;
start_info.command = absl::StrFormat(
"%s "
"%s %s -p -T "
@@ -147,6 +148,7 @@ ProcessStartInfo RemoteUtil::BuildProcessStartInfoForSshInternal(
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_;
start_info.flags = ProcessFlags::kNoWindow;
return start_info;
}