dropshell/runner/src/runner.cpp
Your Name 00571d8091
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 21s
.
2025-05-10 13:28:13 +12:00

500 lines
15 KiB
C++

#include "runner.h"
#include <iostream>
#include <sstream>
#include <cstdlib>
#include <array>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <libssh/libssh.h>
#include <libssh/sftp.h>
#include <pwd.h>
namespace dropshell {
bool Runner::run(const nlohmann::json& run_json) {
std::string command;
std::vector<std::string> args;
std::string working_dir;
std::map<std::string, std::string> env;
bool silent, interactive;
if (!parse_json(run_json, command, args, working_dir, env, silent, interactive)) {
return false;
}
int exit_code;
if (run_json.contains("ssh")) {
exit_code = execute_ssh(run_json["ssh"], command, args, working_dir, env, silent, interactive);
} else {
exit_code = execute_local(command, args, working_dir, env, silent, interactive);
}
return exit_code == 0;
}
bool Runner::run(const nlohmann::json& run_json, std::string& output) {
std::string command;
std::vector<std::string> args;
std::string working_dir;
std::map<std::string, std::string> env;
bool silent, interactive;
if (!parse_json(run_json, command, args, working_dir, env, silent, interactive)) {
return false;
}
int exit_code;
if (run_json.contains("ssh")) {
exit_code = execute_ssh(run_json["ssh"], command, args, working_dir, env, silent, interactive, &output, true);
} else {
exit_code = execute_local(command, args, working_dir, env, silent, interactive, &output, true);
}
return exit_code == 0;
}
bool Runner::parse_json(
const nlohmann::json& run_json,
std::string& command,
std::vector<std::string>& args,
std::string& working_dir,
std::map<std::string, std::string>& env,
bool& silent,
bool& interactive
) {
try {
// Command is required
if (!run_json.contains("command") || !run_json["command"].is_string()) {
std::cerr << "Error: 'command' field is required and must be a string" << std::endl;
return false;
}
command = run_json["command"];
// Args are optional
args.clear();
if (run_json.contains("args")) {
if (!run_json["args"].is_array()) {
std::cerr << "Error: 'args' field must be an array" << std::endl;
return false;
}
for (const auto& arg : run_json["args"]) {
if (!arg.is_string()) {
std::cerr << "Error: All arguments must be strings" << std::endl;
return false;
}
args.push_back(arg);
}
}
// Working directory is optional
working_dir = "";
if (run_json.contains("working_directory")) {
if (!run_json["working_directory"].is_string()) {
std::cerr << "Error: 'working_directory' field must be a string" << std::endl;
return false;
}
working_dir = run_json["working_directory"];
}
// Environment variables are optional
env.clear();
if (run_json.contains("env")) {
if (!run_json["env"].is_object()) {
std::cerr << "Error: 'env' field must be an object" << std::endl;
return false;
}
for (auto it = run_json["env"].begin(); it != run_json["env"].end(); ++it) {
if (!it.value().is_string()) {
std::cerr << "Error: All environment variable values must be strings" << std::endl;
return false;
}
env[it.key()] = it.value();
}
}
// Options are optional
silent = false;
interactive = false;
if (run_json.contains("options")) {
if (!run_json["options"].is_object()) {
std::cerr << "Error: 'options' field must be an object" << std::endl;
return false;
}
if (run_json["options"].contains("silent")) {
if (!run_json["options"]["silent"].is_boolean()) {
std::cerr << "Error: 'silent' option must be a boolean" << std::endl;
return false;
}
silent = run_json["options"]["silent"];
}
if (run_json["options"].contains("interactive")) {
if (!run_json["options"]["interactive"].is_boolean()) {
std::cerr << "Error: 'interactive' option must be a boolean" << std::endl;
return false;
}
interactive = run_json["options"]["interactive"];
}
}
return true;
} catch (const std::exception& e) {
std::cerr << "Error parsing JSON: " << e.what() << std::endl;
return false;
}
}
int Runner::execute_local(
const std::string& command,
const std::vector<std::string>& args,
const std::string& working_dir,
const std::map<std::string, std::string>& env,
bool silent,
bool interactive,
std::string* output,
bool capture_output
) {
int pipefd[2];
if (capture_output && pipe(pipefd) == -1) {
std::cerr << "Error creating pipe: " << strerror(errno) << std::endl;
return -1;
}
pid_t pid = fork();
if (pid == -1) {
std::cerr << "Fork failed: " << strerror(errno) << std::endl;
if (capture_output) {
close(pipefd[0]);
close(pipefd[1]);
}
return -1;
}
if (pid == 0) {
// Child process
// Set up output redirection if needed
if (capture_output) {
close(pipefd[0]); // Close read end
dup2(pipefd[1], STDOUT_FILENO);
dup2(pipefd[1], STDERR_FILENO);
close(pipefd[1]);
} else if (silent) {
int devnull = open("/dev/null", O_WRONLY);
if (devnull != -1) {
dup2(devnull, STDOUT_FILENO);
dup2(devnull, STDERR_FILENO);
close(devnull);
}
}
// Change to working directory if specified
if (!working_dir.empty()) {
if (chdir(working_dir.c_str()) != 0) {
std::cerr << "Error changing to directory " << working_dir << ": " << strerror(errno) << std::endl;
exit(1);
}
}
// Set environment variables
for (const auto& [key, value] : env) {
setenv(key.c_str(), value.c_str(), 1);
}
// Prepare arguments
std::vector<char*> c_args;
c_args.push_back(const_cast<char*>(command.c_str()));
for (const auto& arg : args) {
c_args.push_back(const_cast<char*>(arg.c_str()));
}
c_args.push_back(nullptr);
// Execute command
execvp(command.c_str(), c_args.data());
// If exec fails
std::cerr << "exec failed: " << strerror(errno) << std::endl;
exit(1);
} else {
// Parent process
if (capture_output) {
close(pipefd[1]); // Close write end
std::array<char, 4096> buffer;
std::ostringstream oss;
ssize_t bytes_read;
while ((bytes_read = read(pipefd[0], buffer.data(), buffer.size() - 1)) > 0) {
buffer[bytes_read] = '\0';
oss << buffer.data();
if (!silent) {
std::cout << buffer.data();
}
}
close(pipefd[0]);
if (output) {
*output = oss.str();
}
}
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
return WEXITSTATUS(status);
} else {
return -1;
}
}
}
std::string find_ssh_key_for_user() {
const char* home_dir = getenv("HOME");
if (!home_dir) {
struct passwd* pw = getpwuid(getuid());
if (pw) {
home_dir = pw->pw_dir;
}
}
if (!home_dir) {
return "";
}
// Common SSH key locations
std::vector<std::string> key_paths = {
std::string(home_dir) + "/.ssh/id_rsa",
std::string(home_dir) + "/.ssh/id_ed25519",
std::string(home_dir) + "/.ssh/id_ecdsa",
std::string(home_dir) + "/.ssh/id_dsa"
};
for (const auto& path : key_paths) {
if (access(path.c_str(), F_OK) == 0) {
return path;
}
}
return "";
}
int Runner::execute_ssh(
const nlohmann::json& ssh_config,
const std::string& command,
const std::vector<std::string>& args,
const std::string& working_dir,
const std::map<std::string, std::string>& env,
bool silent,
bool interactive,
std::string* output,
bool capture_output
) {
if (!ssh_config.contains("host") || !ssh_config["host"].is_string()) {
std::cerr << "Error: SSH configuration requires 'host' field" << std::endl;
return -1;
}
std::string host = ssh_config["host"];
int port = 22;
if (ssh_config.contains("port")) {
if (ssh_config["port"].is_number_integer()) {
port = ssh_config["port"];
} else {
std::cerr << "Error: SSH 'port' must be an integer" << std::endl;
return -1;
}
}
std::string user = "";
if (ssh_config.contains("user") && ssh_config["user"].is_string()) {
user = ssh_config["user"];
} else {
// Get current username as default
char username[256];
if (getlogin_r(username, sizeof(username)) == 0) {
user = username;
} else {
struct passwd* pw = getpwuid(getuid());
if (pw) {
user = pw->pw_name;
}
}
}
std::string key_path = "";
if (ssh_config.contains("key") && ssh_config["key"].is_string()) {
std::string key = ssh_config["key"];
if (key == "auto") {
key_path = find_ssh_key_for_user();
if (key_path.empty()) {
std::cerr << "Error: Could not find SSH key automatically" << std::endl;
return -1;
}
} else {
key_path = key;
}
} else {
key_path = find_ssh_key_for_user();
}
// Initialize SSH session
ssh_session session = ssh_new();
if (session == nullptr) {
std::cerr << "Error: Failed to create SSH session" << std::endl;
return -1;
}
// Set SSH options
ssh_options_set(session, SSH_OPTIONS_HOST, host.c_str());
ssh_options_set(session, SSH_OPTIONS_PORT, &port);
ssh_options_set(session, SSH_OPTIONS_USER, user.c_str());
// Connect to server
int rc = ssh_connect(session);
if (rc != SSH_OK) {
std::cerr << "Error connecting to " << host << ": " << ssh_get_error(session) << std::endl;
ssh_free(session);
return -1;
}
// Authenticate with key
if (!key_path.empty()) {
rc = ssh_userauth_publickey_auto(session, nullptr, key_path.empty() ? nullptr : key_path.c_str());
if (rc != SSH_AUTH_SUCCESS) {
std::cerr << "Error authenticating with key: " << ssh_get_error(session) << std::endl;
ssh_disconnect(session);
ssh_free(session);
return -1;
}
} else {
// Try default authentication methods
rc = ssh_userauth_publickey_auto(session, nullptr, nullptr);
if (rc != SSH_AUTH_SUCCESS) {
std::cerr << "Error authenticating: " << ssh_get_error(session) << std::endl;
ssh_disconnect(session);
ssh_free(session);
return -1;
}
}
// Prepare command
std::ostringstream cmd_stream;
// Add environment variables
for (const auto& [key, value] : env) {
cmd_stream << "export " << key << "=\"" << value << "\"; ";
}
// Add cd command if working_directory is specified
if (!working_dir.empty()) {
cmd_stream << "cd \"" << working_dir << "\" && ";
}
// Add command and args
cmd_stream << command;
for (const auto& arg : args) {
cmd_stream << " " << arg;
}
std::string full_command = cmd_stream.str();
int exit_status = -1;
ssh_channel channel = ssh_channel_new(session);
if (channel == nullptr) {
std::cerr << "Error creating SSH channel: " << ssh_get_error(session) << std::endl;
ssh_disconnect(session);
ssh_free(session);
return -1;
}
rc = ssh_channel_open_session(channel);
if (rc != SSH_OK) {
std::cerr << "Error opening SSH session: " << ssh_get_error(session) << std::endl;
ssh_channel_free(channel);
ssh_disconnect(session);
ssh_free(session);
return -1;
}
if (interactive) {
// Request a pseudo-terminal for interactive commands with specific term type
const char* term_type = "xterm-256color";
rc = ssh_channel_request_pty_size(channel, term_type, 80, 24);
if (rc != SSH_OK) {
// Fallback to basic PTY request
std::cerr << "Warning: Could not set PTY with size, falling back to basic PTY" << std::endl;
rc = ssh_channel_request_pty(channel);
if (rc != SSH_OK) {
std::cerr << "Error requesting PTY: " << ssh_get_error(session) << std::endl;
ssh_channel_close(channel);
ssh_channel_free(channel);
ssh_disconnect(session);
ssh_free(session);
return -1;
}
}
}
// Execute the command
rc = ssh_channel_request_exec(channel, full_command.c_str());
if (rc != SSH_OK) {
std::cerr << "Error executing command: " << ssh_get_error(session) << std::endl;
ssh_channel_close(channel);
ssh_channel_free(channel);
ssh_disconnect(session);
ssh_free(session);
return -1;
}
// Read command output
char buffer[4096];
int nbytes;
std::ostringstream oss;
// Read from stdout
while ((nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0)) > 0) {
if (capture_output) {
oss.write(buffer, nbytes);
}
if (!silent) {
std::cout.write(buffer, nbytes);
}
}
// Read from stderr if needed
while ((nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 1)) > 0) {
if (capture_output) {
oss.write(buffer, nbytes);
}
if (!silent) {
std::cerr.write(buffer, nbytes);
}
}
if (capture_output && output) {
*output = oss.str();
}
// Get exit status
ssh_channel_send_eof(channel);
exit_status = ssh_channel_get_exit_status(channel);
// Clean up
ssh_channel_close(channel);
ssh_channel_free(channel);
ssh_disconnect(session);
ssh_free(session);
return exit_status;
}
} // namespace dropshell