500 lines
15 KiB
C++
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
|