blob: 0660048ae0391c7d4ad23d60fcb6bcdb05fe19ef [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/developer/debug/e2e_tests/ffx_debug_agent_bridge.h"
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#include <csignal>
#include <filesystem>
#include <memory>
#include "lib/syslog/cpp/macros.h"
#include "src/developer/debug/shared/logging/logging.h"
#include "src/developer/debug/zxdb/common/err.h"
namespace zxdb {
namespace {
constexpr std::string_view kFuchsiaDeviceAddr = "FUCHSIA_DEVICE_ADDR";
constexpr std::string_view kFuchsiaSshKey = "FUCHSIA_SSH_KEY";
Err KillProcessWithSignal(pid_t pid, int signal) {
if (kill(pid, signal) != 0) {
const std::string s(strerror(errno));
return Err("Failed to send signal " + std::to_string(signal) + " to child process: " + s);
}
int status;
const int wait_pid = wait(&status);
if (wait_pid == -1) {
const std::string s(strerror(errno));
return Err("Failed while waiting for child to terminate: " + s);
}
// This should be the normal case.
if (WIFEXITED(status)) {
return Err();
} else if (WIFSIGNALED(status)) {
FX_LOGS(WARNING) << "Child forced to terminate.";
return Err();
}
return Err("Child exited due to an unexpected signal (" + std::string{strsignal(status)} +
"), this is likely a bug.");
}
std::vector<char*> GetFfxArgV() {
std::vector<char*> ffx_args = {const_cast<char*>("ffx")};
// In infra, this environment variable is populated with the device that's been assigned to the
// infra bot. Locally, a user can also set this to point to a specific device if they choose, but
// `fx set-device` will also work just as well.
char* device_addr = std::getenv(kFuchsiaDeviceAddr.data());
if (device_addr) {
ffx_args.push_back(const_cast<char*>("--target"));
ffx_args.push_back(device_addr);
}
ffx_args.push_back(const_cast<char*>("debug"));
ffx_args.push_back(const_cast<char*>("connect"));
ffx_args.push_back(const_cast<char*>("--agent-only"));
ffx_args.push_back(nullptr); // argv must be null-terminated
return ffx_args;
}
// The environment variable |kFuchsiaSshKey| needs to be a full path for FFX to properly resolve
// the file, but in infra, it's set to a relative path. This function expands the environment
// variable to the full path to the ssh key file, if it exists. Other environment variables are
// copied. The returned strings must be freed properly.
std::vector<char*> GetFfxEnv(char** unix_env) {
std::vector<char*> new_env = {};
// Duplicate the strings in the parent environment for us to manage in the child process. The
// ownership of these strings is kept by the class and they are deallocated when this object goes
// out of scope.
for (size_t i = 0; unix_env[i] != nullptr; i++) {
// Do not duplicate |kFuchsiaSshKey| because we're going to modify it before putting it
// back into place later.
if (strstr(unix_env[i], kFuchsiaSshKey.data()) == nullptr) {
new_env.push_back(strdup(unix_env[i]));
}
}
if (char* ssh_key_path_str = std::getenv(kFuchsiaSshKey.data())) {
std::string ssh_key_env_var{kFuchsiaSshKey};
ssh_key_env_var.append("=");
ssh_key_env_var.append(std::filesystem::absolute(ssh_key_path_str).string());
new_env.push_back(strdup(ssh_key_env_var.data()));
}
new_env.push_back(nullptr);
return new_env;
}
} // namespace
Err FfxDebugAgentBridge::Init() {
Err e = SetupPipeAndFork(unix_env_);
if (e.has_error()) {
return e;
}
e = ReadDebugAgentSocketPath();
if (e.has_error()) {
return e;
}
return e;
}
FfxDebugAgentBridge::~FfxDebugAgentBridge() {
socket_path_.clear();
if (pipe_read_end_ != 0) {
close(pipe_read_end_);
}
if (child_pid_ != 0) {
Err e = CleanupChild();
if (e.has_error()) {
FX_LOGS(ERROR) << "Error encountered while cleaning up child: " << e.msg();
}
}
}
Err FfxDebugAgentBridge::SetupPipeAndFork(char** unix_env) {
int p[2];
if (pipe(p) < 0) {
const std::string s(strerror(errno));
return Err("Could not create pipe: " + s);
}
pipe_read_end_ = p[0];
pipe_write_end_ = p[1];
// HACK: try to get the ffx daemon up and running before we setup debug_agent. I'm not sure if
// this will help.
// for (size_t i = 0; i < 5; i++) {
// if (system("ffx target get-ssh-address") == 0) {
// break;
// }
// usleep(5000);
// }
const pid_t child_pid = fork();
if (child_pid == 0) {
close(pipe_read_end_);
const std::filesystem::path me(program_name_);
// In variant builds that put the test executable in a different directory (potentially
// something like out/default/host_x64-asan/...), ffx could be in a different directory than the
// test executable.
const std::filesystem::path ffx_path = me.parent_path().parent_path() / "host_x64" / "ffx";
if (!std::filesystem::exists(ffx_path)) {
FX_LOGS(ERROR) << "Could not locate ffx binary at " << std::filesystem::absolute(ffx_path);
FX_LOGS(ERROR) << "Note: zxdb_e2e_tests binary is at " << std::filesystem::absolute(me);
exit(EXIT_FAILURE);
}
// |pipe_write_end_| will be closed along with stdout when the child program
// terminates.
if (dup2(pipe_write_end_, STDOUT_FILENO) < 0) {
FX_LOGS(ERROR) << "Failed to dup child stdout to pipe write end: " << strerror(errno);
exit(EXIT_FAILURE);
}
std::vector<char*> env = GetFfxEnv(unix_env);
execve(ffx_path.c_str(), GetFfxArgV().data(), env.data());
FX_NOTREACHED();
} else {
close(pipe_write_end_);
child_pid_ = child_pid;
}
return Err();
}
Err FfxDebugAgentBridge::ReadDebugAgentSocketPath() {
FILE* child_stdout = fdopen(pipe_read_end_, "r");
if (child_stdout == nullptr) {
const std::string s(strerror(errno));
return Err("Failed to open pipe_read_end_ fd as FILE*: " + s);
}
char c;
size_t bytes_read = fread(&c, 1, 1, child_stdout);
while (c != '\n') {
if (bytes_read == 0) {
Err e;
if (int err = feof(child_stdout); err != 0) {
e = Err("Unexpected EOF while reading stdout from child process " + std::to_string(err));
} else if (int err = ferror(child_stdout); err != 0) {
e = Err("Unexpected error while reading stdout from child process " + std::to_string(err));
} else {
e = Err("Unknown error occurred while reading from child process, got 0 bytes from fread");
}
return e;
}
socket_path_.push_back(c);
bytes_read = fread(&c, 1, 1, child_stdout);
}
fclose(child_stdout);
// Now check to make sure this is actually a path.
std::filesystem::path ffx_path(socket_path_);
if (!std::filesystem::exists(ffx_path)) {
return Err("Output of \"ffx debug connect --agent-only\" is not a valid path: " + socket_path_);
}
return Err();
}
Err FfxDebugAgentBridge::CleanupChild() const {
Err e = KillProcessWithSignal(child_pid_, SIGTERM);
if (e.has_error()) {
FX_LOGS(WARNING) << "Failed to kill child [" << child_pid_ << "] with SIGTERM, trying SIGKILL.";
e = KillProcessWithSignal(child_pid_, SIGKILL);
if (e.has_error()) {
FX_LOGS(ERROR) << "Failed to kill child with SIGKILL. There is a zombie process with pid "
<< child_pid_;
}
}
return e;
}
} // namespace zxdb