Integrating runner.
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 23s

This commit is contained in:
Your Name
2025-05-10 15:24:53 +12:00
parent ec5f4ad38d
commit 35c97728c9
15 changed files with 96 additions and 26403 deletions

View File

@ -5,7 +5,9 @@
#include "templates.hpp"
#include "utils/utils.hpp"
#include "utils/json.hpp"
#include "utils/execute.hpp"
#include "utils/runner.hpp"
#include <libassert/assert.hpp>
#include <iostream>
#include <memory>
@ -102,94 +104,82 @@ std::string server_env_manager::get_variable(const std::string& name) const {
return it->second;
}
sCommand server_env_manager::construct_standard_template_run_cmd(const std::string &service_name, const std::string &command, std::vector<std::string> args, bool silent) const
{
if (command.empty())
return sCommand();
// sCommand server_env_manager::construct_standard_template_run_cmd(const std::string &service_name, const std::string &command, std::vector<std::string> args, bool silent) const
// {
// if (command.empty())
// return sCommand();
std::string remote_service_template_path = remotepath::service_template(mServerName,service_name);
std::string script_path = remote_service_template_path + "/" + command + ".sh";
// std::string remote_service_template_path = remotepath::service_template(mServerName,service_name);
// std::string script_path = remote_service_template_path + "/" + command + ".sh";
std::map<std::string, std::string> env_vars;
if (!get_all_service_env_vars(mServerName, service_name, env_vars)) {
std::cerr << "Error: Failed to get all service env vars for " << service_name << std::endl;
return sCommand();
}
// std::map<std::string, std::string> env_vars;
// if (!get_all_service_env_vars(mServerName, service_name, env_vars)) {
// std::cerr << "Error: Failed to get all service env vars for " << service_name << std::endl;
// return sCommand();
// }
std::string argstr = "";
for (const auto& arg : args) {
argstr += " " + quote(dequote(trim(arg)));
}
// std::string argstr = "";
// for (const auto& arg : args) {
// argstr += " " + quote(dequote(trim(arg)));
// }
sCommand scommand(remote_service_template_path, "bash " + quote(script_path) + argstr + (silent ? " > /dev/null 2>&1" : ""), env_vars);
// sCommand scommand(remote_service_template_path, "bash " + quote(script_path) + argstr + (silent ? " > /dev/null 2>&1" : ""), env_vars);
if (scommand.empty())
std::cerr << "Error: Failed to construct command for " << service_name << " " << command << std::endl;
// if (scommand.empty())
// std::cerr << "Error: Failed to construct command for " << service_name << " " << command << std::endl;
return scommand;
}
// return scommand;
// }
bool server_env_manager::check_remote_dir_exists(const std::string &dir_path) const
{
sCommand scommand("test -d " + quote(dir_path));
return execute_ssh_command(get_SSH_INFO(), scommand, cMode::Silent);
return 0==runner::execute_cmd("test",{"-d", quote(dir_path)}, {}, {}, true, false, &get_SSH_INFO());
}
bool server_env_manager::check_remote_file_exists(const std::string& file_path) const {
sCommand scommand("test -f " + quote(file_path));
return execute_ssh_command(get_SSH_INFO(), scommand, cMode::Silent);
return 0==runner::execute_cmd("test",{"-f",quote(file_path)}, {}, {}, true, false, &get_SSH_INFO());
}
bool server_env_manager::check_remote_items_exist(const std::vector<std::string> &file_paths) const
{
// convert file_paths to a single string, separated by spaces
std::string file_paths_str;
std::string file_names_str;
for (const auto& file_path : file_paths) {
file_paths_str += quote(file_path) + " ";
file_names_str += std::filesystem::path(file_path).filename().string() + " ";
}
// check if all items in the vector exist on the remote server, in a single command.
sCommand scommand("for item in " + file_paths_str + "; do test -f $item; done");
bool okay = execute_ssh_command(get_SSH_INFO(), scommand, cMode::Silent);
if (!okay) {
std::cerr << "Error: Required items not found on remote server: " << file_names_str << std::endl;
return 0==runner::execute_cmd("bash",{"-c","for item in " + file_paths_str + "; do test -f $item; done"}, {}, {}, true, false, &get_SSH_INFO());
}
bool server_env_manager::run_remote_template_command(const std::string &service_name, const std::string &command, std::vector<std::string> args, bool silent, std::map<std::string, std::string> extra_env_vars, std::string * output) const
{
std::string working_dir = remotepath::service_template(mServerName,service_name);
std::string script_path = working_dir + "/" + command + ".sh";
std::string argstr = "";
for (const auto& arg : args) {
argstr += " " + quote(dequote(trim(arg)));
}
std::map<std::string, std::string> env_vars;
if (!get_all_service_env_vars(mServerName, service_name, env_vars)) {
std::cerr << "Error: Failed to get all service env vars for " << service_name << std::endl;
return false;
}
return true;
}
bool server_env_manager::run_remote_template_command(const std::string &service_name, const std::string &command, std::vector<std::string> args, bool silent, std::map<std::string, std::string> extra_env_vars) const
{
sCommand scommand = construct_standard_template_run_cmd(service_name, command, args, silent);
// add the extra env vars to the command
for (const auto& [key, value] : extra_env_vars)
scommand.add_env_var(key, value);
env_vars[key] = value;
if (scommand.get_command_to_run().empty())
return false;
cMode mode = (command=="ssh") ? (cMode::Interactive | cMode::RawCommand) : cMode::Silent;
return execute_ssh_command(get_SSH_INFO(), scommand, mode);
bool interactive = (command=="ssh");
ASSERT(!output || !silent); // if output is captured, silent must be false
ASSERT(!interactive || !silent); // if command is ssh, silent must be false
return 0==runner::execute_cmd("bash",{"-c", quote(script_path) + argstr}, working_dir, env_vars, silent, interactive, &get_SSH_INFO(), output);
}
bool server_env_manager::run_remote_template_command_and_capture_output(const std::string &service_name, const std::string &command, std::vector<std::string> args, std::string &output, bool silent, std::map<std::string, std::string> extra_env_vars) const
{
sCommand scommand = construct_standard_template_run_cmd(service_name, command, args, false);
if (scommand.get_command_to_run().empty())
return false;
// add the extra env vars to the command
for (const auto& [key, value] : extra_env_vars)
scommand.add_env_var(key, value);
cMode mode = cMode::CaptureOutput | cMode::RawCommand;
return execute_ssh_command(get_SSH_INFO(), scommand, mode, &output);
}
// base64 <<< "FOO=BAR WHEE=YAY bash ./test.sh"
// echo YmFzaCAtYyAnRk9PPUJBUiBXSEVFPVlBWSBiYXNoIC4vdGVzdC5zaCcK | base64 -d | bash

View File

@ -9,7 +9,6 @@
#include <map>
#include <memory>
#include <vector>
#include "utils/execute.hpp"
namespace dropshell {
@ -43,7 +42,7 @@ class server_env_manager {
std::string get_SSH_USER() const { return get_variable("SSH_USER"); }
std::string get_SSH_PORT() const { return get_variable("SSH_PORT"); }
std::string get_DROPSHELL_DIR() const { return get_variable("DROPSHELL_DIR"); }
sSSHInfo get_SSH_INFO() const { return sSSHInfo{get_SSH_HOST(), get_SSH_USER(), get_SSH_PORT()}; }
runner::sSSHInfo get_SSH_INFO() const { return runner::sSSHInfo{get_SSH_HOST(), get_SSH_USER(), get_SSH_PORT()}; }
bool is_valid() const { return mValid; }
std::string get_server_name() const { return mServerName; }
@ -54,12 +53,10 @@ class server_env_manager {
bool check_remote_items_exist(const std::vector<std::string>& file_paths) const;
bool run_remote_template_command(const std::string& service_name, const std::string& command,
std::vector<std::string> args, bool silent, std::map<std::string, std::string> extra_env_vars) const;
bool run_remote_template_command_and_capture_output(const std::string& service_name, const std::string& command,
std::vector<std::string> args, std::string & output, bool silent, std::map<std::string, std::string> extra_env_vars) const;
std::vector<std::string> args = {}, bool silent = false, std::map<std::string, std::string> extra_env_vars = {}, std::string * output = nullptr) const;
private:
sCommand construct_standard_template_run_cmd(const std::string& service_name, const std::string& command, std::vector<std::string> args, bool silent) const;
//sCommand construct_standard_template_run_cmd(const std::string& service_name, const std::string& command, std::vector<std::string> args, bool silent) const;
private:
std::string mServerName;

View File

@ -1,4 +1,3 @@
#include <iostream>
#include <fstream>
#include <sstream>
@ -16,7 +15,7 @@
#include "services.hpp"
#include "utils/directories.hpp"
#include "utils/utils.hpp"
#include "utils/runner.hpp"
namespace fs = std::filesystem;
@ -54,17 +53,14 @@ bool service_runner::install(bool silent) {
return false;
// Create service directory
std::string remote_service_path = remotepath::service(mServer, mService);
std::string mkdir_cmd = "mkdir -p " + quote(remote_service_path);
if (!execute_ssh_command(mServerEnv.get_SSH_INFO(), sCommand(mkdir_cmd), cMode::Silent))
if (0!=runner::execute_cmd("mkdir -p " + remotepath::service(mServer, mService),{},"",{},true,false,&mServerEnv.get_SSH_INFO()))
{
std::cerr << "Failed to create service directory " << remote_service_path << std::endl;
std::cerr << "Failed to create service directory " << remotepath::service(mServer, mService) << std::endl;
return false;
}
// Check if rsync is installed on remote host
std::string check_rsync_cmd = "which rsync > /dev/null 2>&1";
if (!execute_ssh_command(mServerEnv.get_SSH_INFO(), sCommand(check_rsync_cmd), cMode::Silent))
if (0!=runner::execute_cmd("which rsync",{},"",{},true,false,&mServerEnv.get_SSH_INFO()))
{
std::cerr << "rsync is not installed on the remote host" << std::endl;
return false;
@ -72,16 +68,7 @@ bool service_runner::install(bool silent) {
// Copy template files
{
std::cout << "Copying: [LOCAL] " << tinfo.local_template_path() << std::endl << std::string(8,' ')<<"[REMOTE] " << remotepath::service_template(mServer, mService) << "/" << std::endl;
std::string rsync_cmd = "rsync --delete -zrpc -e 'ssh -p " + mServerEnv.get_SSH_PORT() + "' " +
quote(tinfo.local_template_path().string()+"/") + " "+
mServerEnv.get_SSH_USER() + "@" + mServerEnv.get_SSH_HOST() + ":" +
quote(remotepath::service_template(mServer, mService)+"/");
//std::cout << std::endl << rsync_cmd << std::endl << std::endl;
if (!execute_local_command(rsync_cmd, silent ? cMode::Silent : cMode::None))
{
std::cerr << "Failed to copy template files using rsync" << std::endl;
std::cerr << "Is rsync installed on the remote host?" << std::endl;
if (!rsync_copy(tinfo.local_template_path().string()+"/", remotepath::service_template(mServer, mService)+"/", silent)) {
return false;
}
}
@ -93,14 +80,8 @@ bool service_runner::install(bool silent) {
std::cerr << "Error: Service directory not found: " << local_service_path << std::endl;
return false;
}
std::cout << "Copying: [LOCAL] " << local_service_path << std::endl <<std::string(8,' ')<<"[REMOTE] " << remotepath::service_config(mServer,mService) << std::endl;
std::string rsync_cmd = "rsync --delete -zrpc -e 'ssh -p " + mServerEnv.get_SSH_PORT() + "' " +
quote(local_service_path + "/") + " "+
mServerEnv.get_SSH_USER() + "@" + mServerEnv.get_SSH_HOST() + ":" +
quote(remotepath::service_config(mServer,mService) + "/");
if (!execute_local_command(rsync_cmd, silent ? cMode::Silent : cMode::None))
{
std::cerr << "Failed to copy service files using rsync" << std::endl;
if (!rsync_copy(local_service_path + "/", remotepath::service_config(mServer,mService) + "/", silent)) {
return false;
}
}
@ -141,12 +122,9 @@ bool service_runner::uninstall(bool silent) {
}
// 4. Remove the service directory from the server
std::string rm_cmd = "rm -rf " + quote(remotepath::service(mServer, mService));
if (!execute_ssh_command(mServerEnv.get_SSH_INFO(), sCommand(rm_cmd), cMode::Silent)) {
std::cerr << "Failed to remove service directory" << std::endl;
return false;
}
if (0!=runner::execute_cmd("rm -rf " + quote(remotepath::service(mServer, mService)), {}, "", {}, true, false, &mServerEnv.get_SSH_INFO()))
std::cerr << "Failed to remove remote service directory at " << remotepath::service(mServer, mService) << std::endl;
std::cout << "Service " << mService << " successfully uninstalled from " << mServer << std::endl;
return true;
}
@ -189,12 +167,10 @@ bool service_runner::fullnuke()
return false;
}
std::string rm_cmd = "rm -rf " + quote(local_service_path);
if (!execute_local_command(rm_cmd, cMode::Silent)) {
std::cerr << "Failed to remove service directory" << std::endl;
return false;
}
if (0!=runner::execute_cmd("rm -rf " + quote(local_service_path), {}, "", {}, true, false, &mServerEnv.get_SSH_INFO()))
std::cerr << "Failed to remove local service directory at " << local_service_path << std::endl;
std::cout << "Service " << mService << " successfully fully nuked from " << mServer << std::endl;
return true;
}
@ -296,15 +272,13 @@ std::map<std::string, ServiceStatus> service_runner::get_all_services_status(std
return status;
}
std::string remote_service_template_path = remotepath::service_template(server_name,service_name);
std::string script_path = remote_service_template_path + "/shared/" + command + ".sh";
sCommand scommand(remote_service_template_path, "bash " + quote(script_path), {});
std::string cmd_path = remotepath::service_template(server_name,service_name) + "/shared/";
std::string output;
cMode mode = cMode::CaptureOutput | cMode::RawCommand;
if (!execute_ssh_command(env.get_SSH_INFO(), scommand, mode, &output))
if (0!=runner::execute_cmd("bash",{cmd_path+command+".sh"}, cmd_path, {}, true, false, &env.get_SSH_INFO(), &output))
{
std::cerr << "Error: Failed to run command script at " << cmd_path << std::endl;
return status;
}
std::stringstream ss(output);
std::string line;
@ -772,4 +746,20 @@ std::string service_runner::get_latest_backup_file(const std::string& server, co
return latest_file;
}
bool service_runner::rsync_copy(const std::string& local_path, const std::string& remote_path, bool silent) {
std::cout << "Copying: [LOCAL] " << local_path << std::endl << std::string(8,' ')<<"[REMOTE] " << remote_path << std::endl;
std::string rsync_cmd = "rsync --delete -zrpc -e 'ssh -p " + mServerEnv.get_SSH_PORT() + "' " +
quote(local_path) + " " +
mServerEnv.get_SSH_USER() + "@" + mServerEnv.get_SSH_HOST() + ":" +
quote(remote_path);
if (0 != runner::execute_cmd(rsync_cmd, {}, "", {}, true, false, &mServerEnv.get_SSH_INFO())) {
std::cerr << "Failed to copy files using rsync" << std::endl;
std::cerr << "Is rsync installed on the remote host?" << std::endl;
return false;
}
return true;
}
} // namespace dropshell

View File

@ -87,6 +87,8 @@ class service_runner {
// edit the service configuration file
void edit_service_config();
// Helper methods
bool rsync_copy(const std::string& local_path, const std::string& remote_path, bool silent);
public:
// utility functions

View File

@ -1,182 +0,0 @@
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <vector>
#include <iostream>
#include <string>
#include <cstdlib>
#include <sstream>
#include <libassert/assert.hpp>
#include "execute.hpp"
#include "contrib/base64.hpp"
#include "utils/utils.hpp"
bool EXITSTATUSCHECK(int ret) {
return (ret != -1 && WIFEXITED(ret) && (WEXITSTATUS(ret) == 0)); // ret is -1 if the command failed to execute.
}
namespace dropshell {
bool execute_local_command_interactive(const sCommand &command, bool silent)
{
if (command.get_command_to_run().empty())
return false;
std::string full_command = command.construct_cmd(cStyle::Raw); // Get the command string
pid_t pid = fork();
if (pid == -1) {
// Fork failed
perror("fork failed");
return false;
} else if (pid == 0) {
// Child process
std::vector<const char *> commandvec = {"bash", "-c", full_command.c_str(),NULL};
if (!silent) {
std::cout << "Executing command: ";
for (auto & x : commandvec) std::cout << x << " ";
std::cout << std::endl;
}
execvp(commandvec[0], const_cast<char* const*>(commandvec.data()));
// If execvp returns, it means an error occurred
perror("execvp failed");
exit(EXIT_FAILURE); // Exit child process on error
} else {
// Parent process
int ret;
// Wait for the child process to complete
waitpid(pid, &ret, 0);
return EXITSTATUSCHECK(ret);
}
}
bool execute_local_command_and_capture_output(const sCommand& command, std::string * output, cMode mode)
{
ASSERT(output != nullptr, "Output string must be provided");
ASSERT(is_raw(mode), "Capture output mode requires raw command mode");
ASSERT(!hasFlag(mode, cMode::Silent), "Silent mode is not allowed with capture output mode");
if (command.get_command_to_run().empty())
return false;
cStyle style = getStyle(mode);
std::string full_cmd = command.construct_cmd(style) + " 2>&1";
FILE *pipe = popen(full_cmd.c_str(), "r");
if (!pipe) {
return false;
}
char buffer[128];
while (fgets(buffer, sizeof(buffer), pipe) != nullptr) {
(*output) += buffer;
}
int ret = pclose(pipe);
return EXITSTATUSCHECK(ret);
}
bool execute_local_command(const sCommand & command, cMode mode, std::string * output /* = nullptr */)
{
if (hasFlag(mode, cMode::Interactive)) {
ASSERT(! hasFlag(mode, cMode::CaptureOutput), "Interactive mode and capture output mode cannot be used together");
ASSERT(output == nullptr, "Interactive mode and an output string cannot be used together");
ASSERT(is_raw(mode), "Interactive mode requires raw command mode");
return execute_local_command_interactive(command, hasFlag(mode, cMode::Silent));
}
if (hasFlag(mode, cMode::CaptureOutput)) {
ASSERT(output != nullptr, "Capture output mode requires an output string to be provided");
ASSERT(is_raw(mode), "Capture output mode requires raw command mode");
ASSERT(!hasFlag(mode, cMode::Silent), "Silent mode is not allowed with capture output mode");
return execute_local_command_and_capture_output(command, output, mode);
}
if (command.get_command_to_run().empty())
return false;
cStyle style = getStyle(mode);
std::string full_cmd = command.construct_cmd(style) + " 2>&1" + (hasFlag(mode, cMode::Silent) ? " > /dev/null" : "");
int ret = system(full_cmd.c_str());
bool ok = EXITSTATUSCHECK(ret);
if (!ok) {
std::cerr << "Error: Failed to execute command: " << std::endl;
std::cerr << full_cmd << std::endl;
}
return ok;
}
bool execute_ssh_command(const sSSHInfo &ssh_info, const sCommand &command, cMode mode, std::string *output)
{
if (command.get_command_to_run().empty())
return false;
ASSERT(!(hasFlag(mode, cMode::Interactive) && !is_raw(mode)), "Interactive mode requires raw command mode");
ASSERT(!(hasFlag(mode, cMode::CaptureOutput) && output == nullptr), "Capture output mode must be used with an output string");
std::stringstream ssh_cmd;
ssh_cmd << "ssh -p " << ssh_info.port << " " << (hasFlag(mode, cMode::Interactive) ? "-tt " : "")
<< ssh_info.user << "@" << ssh_info.host;
std::string cmdstr;
if (!is_raw(mode))
cmdstr = quote("bash -c " + command.construct_cmd(cStyle::Safe));
else
{
std::string raw_cmd = command.construct_cmd(cStyle::Raw);
ASSERT(raw_cmd.find("'") == std::string::npos, "Raw command must not contain single quotes");
cmdstr = "bash -c "+ halfquote(raw_cmd);
}
sCommand ssh_command(ssh_cmd.str() + " " + cmdstr);
bool rval = execute_local_command(ssh_command, mode, output);
if (!rval) {
std::cerr <<std::endl<<std::endl;
std::cerr << "Error: Failed to execute ssh command: { [" << ssh_command.get_directory_to_run_in() << "], [";
std::cerr << ssh_command.get_command_to_run() << "], [";
for (const auto& env_var : ssh_command.get_env_vars()) {
std::cerr << env_var.first << "=" << env_var.second << ", ";
}
std::cerr << "] }" << std::endl;
std::cerr <<std::endl<<std::endl;
}
return rval;
}
std::string makesafecmd(const std::string &command)
{
if (command.empty())
return "";
std::string encoded = base64_encode(dequote(trim(command)));
std::string commandstr = "echo " + encoded + " | base64 -d | bash";
return commandstr;
}
std::string sCommand::construct_cmd(cStyle style) const
{
if (mCmd.empty())
return "";
std::string cdcmd;
if (!mDir.empty())
cdcmd = "cd " + quote(mDir) + " && ";
std::string cmdstr;
for (const auto& env_var : mVars) {
cmdstr += env_var.first + "=" + quote(dequote(trim(env_var.second))) + " ";
}
cmdstr += mCmd;
if (is_safe(style))
cmdstr = makesafecmd(cmdstr);
return cdcmd + cmdstr;
}
} // namespace dropshell

View File

@ -1,83 +0,0 @@
#ifndef EXECUTE_HPP
#define EXECUTE_HPP
#include <string>
#include <map>
namespace dropshell {
class sCommand;
// mode bitset
enum class cMode {
None = 0,
Interactive = 1,
Silent = 2,
CaptureOutput = 4,
RawCommand = 8
};
enum class cStyle {
Safe = 0,
Raw = 1
};
inline cMode operator&(cMode lhs, cMode rhs) {return static_cast<cMode>(static_cast<int>(lhs) & static_cast<int>(rhs));}
inline cMode operator+(cMode lhs, cMode rhs) {return static_cast<cMode>(static_cast<int>(lhs) | static_cast<int>(rhs));}
inline cMode operator-(cMode lhs, cMode rhs) {return static_cast<cMode>(static_cast<int>(lhs) & ~static_cast<int>(rhs));}
inline cMode operator|(cMode lhs, cMode rhs) {return static_cast<cMode>(static_cast<int>(lhs) | static_cast<int>(rhs));}
inline cMode operator|=(cMode & lhs, cMode rhs) {return lhs = lhs | rhs;}
inline bool hasFlag(cMode mode, cMode flag) {return (mode & flag) == flag;}
inline bool is_safe(cStyle style) { return style == cStyle::Safe; }
inline bool is_raw(cStyle style) { return style == cStyle::Raw; }
inline bool is_raw(cMode mode) { return hasFlag(mode, cMode::RawCommand); }
inline cStyle getStyle(cMode mode) { return is_raw(mode) ? cStyle::Raw : cStyle::Safe; }
typedef struct sSSHInfo {
std::string host;
std::string user;
std::string port;
} sSSHInfo;
bool execute_local_command(const sCommand & command, cMode mode = cMode::None, std::string * output = nullptr);
bool execute_ssh_command(const sSSHInfo & ssh_info, const sCommand & command, cMode mode = cMode::None, std::string * output = nullptr);
std::string makesafecmd(const std::string& command);
// ------------------------------------------------------------------------------------------------
// class to hold a command to run on the remote server.
class sCommand {
public:
sCommand(std::string directory_to_run_in, std::string command_to_run, const std::map<std::string, std::string> & env_vars) :
mDir(directory_to_run_in), mCmd(command_to_run), mVars(env_vars) {}
sCommand(std::string command_to_run) :
mDir(""), mCmd(command_to_run), mVars({}) {}
sCommand() : mDir(""), mCmd(""), mVars({}) {}
std::string get_directory_to_run_in() const { return mDir; }
std::string get_command_to_run() const { return mCmd; }
const std::map<std::string, std::string>& get_env_vars() const { return mVars; }
void add_env_var(const std::string& key, const std::string& value) { mVars[key] = value; }
std::string construct_cmd(cStyle style) const;
bool empty() const { return mCmd.empty(); }
private:
std::string mDir;
std::string mCmd;
std::map<std::string, std::string> mVars;
};
} // namespace dropshell
#endif

402
src/utils/runner.cpp Normal file
View File

@ -0,0 +1,402 @@
#include "runner.hpp"
#include <cstdlib>
#include <sstream>
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <libssh/libssh.h>
#include <libssh/callbacks.h>
#include <termios.h>
#include <sys/select.h>
#include <algorithm>
#include <cctype>
namespace runner {
namespace {
// String trimming functions
void ltrim(std::string& s) {
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) {
return !std::isspace(ch);
}));
}
void rtrim(std::string& s) {
s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) {
return !std::isspace(ch);
}).base(), s.end());
}
void trim(std::string& s) {
ltrim(s);
rtrim(s);
}
// Safe version that handles nullptr
void trim(std::string* s) {
if (s) {
trim(*s);
}
}
ssh_session ssh_connect_and_auth(const sSSHInfo* sshinfo, const std::map<std::string, std::string>& env, std::string* error) {
ssh_session session = ssh_new();
if (!session) {
if (error) *error = "Failed to create SSH session.";
return nullptr;
}
ssh_options_set(session, SSH_OPTIONS_HOST, sshinfo->host.c_str());
if (!sshinfo->port.empty()) {
int port = std::stoi(sshinfo->port);
ssh_options_set(session, SSH_OPTIONS_PORT, &port);
}
if (!sshinfo->user.empty()) {
ssh_options_set(session, SSH_OPTIONS_USER, sshinfo->user.c_str());
}
int rc = ssh_connect(session);
if (rc != SSH_OK) {
if (error) *error = std::string("SSH connection failed: ") + ssh_get_error(session);
ssh_free(session);
return nullptr;
}
rc = ssh_userauth_publickey_auto(session, nullptr, nullptr);
if (rc != SSH_AUTH_SUCCESS) {
auto it = env.find("SSHPASS");
if (it != env.end()) {
rc = ssh_userauth_password(session, nullptr, it->second.c_str());
}
}
if (rc != SSH_AUTH_SUCCESS) {
if (error) *error = std::string("SSH authentication failed: ") + ssh_get_error(session);
ssh_disconnect(session);
ssh_free(session);
return nullptr;
}
return session;
}
std::string ssh_build_remote_command(const std::string& command, const std::vector<std::string>& args, const std::string& working_dir, const std::map<std::string, std::string>& env) {
std::ostringstream remote_cmd;
for (const auto& kv : env) {
if (kv.first == "SSHPASS") continue;
remote_cmd << kv.first << "='" << kv.second << "' ";
}
if (!working_dir.empty()) {
remote_cmd << "cd '" << working_dir << "' && ";
}
remote_cmd << command;
for (const auto& arg : args) {
remote_cmd << " '" << arg << "'";
}
return remote_cmd.str();
}
// Utility function to escape special shell characters
std::string escape_shell_arg(const std::string& arg) {
std::ostringstream escaped;
escaped << '"';
for (char c : arg) {
if (c == '"' || c == '\\' || c == '$' || c == '`') {
escaped << '\\';
}
escaped << c;
}
escaped << '"';
return escaped.str();
}
// For non-interactive SSH, just build the command with args
std::string ssh_build_command_only(const std::string& command, const std::vector<std::string>& args) {
std::ostringstream remote_cmd;
remote_cmd << command;
for (const auto& arg : args) {
remote_cmd << " " << escape_shell_arg(arg);
}
return remote_cmd.str();
}
int ssh_interactive_shell_session(ssh_session session, ssh_channel channel, const std::string& remote_cmd_str, const std::string& command, std::string* output) {
int rc = ssh_channel_request_pty(channel);
if (rc != SSH_OK) {
if (output) *output = std::string("Failed to request pty: ") + ssh_get_error(session);
return -1;
}
rc = ssh_channel_request_shell(channel);
if (rc != SSH_OK) {
if (output) *output = std::string("Failed to request shell: ") + ssh_get_error(session);
return -1;
}
struct termios orig_termios, raw_termios;
tcgetattr(STDIN_FILENO, &orig_termios);
raw_termios = orig_termios;
cfmakeraw(&raw_termios);
tcsetattr(STDIN_FILENO, TCSANOW, &raw_termios);
if (!command.empty()) {
ssh_channel_write(channel, remote_cmd_str.c_str(), remote_cmd_str.size());
ssh_channel_write(channel, "\n", 1);
}
int maxfd = STDIN_FILENO > STDOUT_FILENO ? STDIN_FILENO : STDOUT_FILENO;
maxfd = maxfd > ssh_get_fd(session) ? maxfd : ssh_get_fd(session);
char buffer[4096];
bool done = false;
while (!done) {
fd_set fds_read;
FD_ZERO(&fds_read);
FD_SET(STDIN_FILENO, &fds_read);
FD_SET(ssh_get_fd(session), &fds_read);
int ret = select(maxfd + 1, &fds_read, nullptr, nullptr, nullptr);
if (ret < 0) break;
if (FD_ISSET(STDIN_FILENO, &fds_read)) {
ssize_t n = read(STDIN_FILENO, buffer, sizeof(buffer));
if (n > 0) {
ssh_channel_write(channel, buffer, n);
} else {
ssh_channel_send_eof(channel);
done = true;
}
}
if (FD_ISSET(ssh_get_fd(session), &fds_read)) {
int n = ssh_channel_read(channel, buffer, sizeof(buffer), 0);
if (n > 0) {
write(STDOUT_FILENO, buffer, n);
} else if (n == 0) {
done = true;
}
}
if (ssh_channel_is_closed(channel) || ssh_channel_is_eof(channel)) {
done = true;
}
}
tcsetattr(STDIN_FILENO, TCSANOW, &orig_termios);
return 0;
}
int ssh_exec_command(ssh_session session, ssh_channel channel, const std::string& remote_cmd_str, bool silent, std::string* output, const std::map<std::string, std::string>& env, const std::string& working_dir) {
// Build complete command with env, working_dir, and the command itself
std::ostringstream cmd_with_env;
// Create a simple, flat command that will work reliably
// Format: env VAR=value bash -c 'cd /path && command args'
// Start with env variables
if (!env.empty()) {
cmd_with_env << "env ";
for (const auto& kv : env) {
if (kv.first == "SSHPASS") continue;
cmd_with_env << kv.first << "='" << kv.second << "' ";
}
}
// Use a single bash -c with the entire command inside single quotes
cmd_with_env << "bash -c '";
// Add cd if working directory specified
if (!working_dir.empty()) {
cmd_with_env << "cd " << working_dir << " && ";
}
// Add the command, but replace any single quotes with '\''
std::string escaped_cmd = remote_cmd_str;
size_t pos = 0;
while ((pos = escaped_cmd.find('\'', pos)) != std::string::npos) {
escaped_cmd.replace(pos, 1, "'\\''");
pos += 4; // Length of "'\\''"
}
cmd_with_env << escaped_cmd;
// Close the single quote
cmd_with_env << "'";
std::string final_cmd = cmd_with_env.str();
int rc = ssh_channel_request_exec(channel, final_cmd.c_str());
if (rc != SSH_OK) {
std::string error = std::string("Failed to exec remote command: ") + ssh_get_error(session);
std::cerr << "SSH exec error: " << error << std::endl;
if (output) *output = error;
return -1;
}
if (output) {
std::ostringstream oss;
char buffer[4096];
int nbytes;
// Read from stdout
while ((nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0)) > 0) {
oss.write(buffer, nbytes);
}
if (nbytes < 0) {
std::cerr << "Error reading from stdout" << std::endl;
}
// Read from stderr
while ((nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 1)) > 0) {
oss.write(buffer, nbytes);
}
if (nbytes < 0) {
std::cerr << "Error reading from stderr" << std::endl;
}
*output = oss.str();
} else if (!silent) {
char buffer[4096];
int nbytes;
// Read from stdout
while ((nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0)) > 0) {
write(1, buffer, nbytes);
}
if (nbytes < 0) {
std::cerr << "Error reading from stdout" << std::endl;
}
// Read from stderr
while ((nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 1)) > 0) {
write(2, buffer, nbytes);
}
if (nbytes < 0) {
std::cerr << "Error reading from stderr" << std::endl;
}
}
return 0;
}
int local_execute_cmd(
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
) {
int pipefd[2];
bool use_pipe = output && !interactive;
if (use_pipe && pipe(pipefd) == -1) {
perror("pipe");
return -1;
}
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return -1;
}
if (pid == 0) {
if (!working_dir.empty()) {
if (chdir(working_dir.c_str()) != 0) {
perror("chdir");
exit(-1);
}
}
for (const auto& kv : env) {
setenv(kv.first.c_str(), kv.second.c_str(), 1);
}
if (use_pipe) {
close(pipefd[0]);
dup2(pipefd[1], STDOUT_FILENO);
dup2(pipefd[1], STDERR_FILENO);
close(pipefd[1]);
} else if (silent && !interactive) {
int devnull = open("/dev/null", O_WRONLY);
dup2(devnull, STDOUT_FILENO);
dup2(devnull, STDERR_FILENO);
close(devnull);
}
if (!interactive) {
setsid();
}
std::vector<char*> argv;
argv.push_back(const_cast<char*>(command.c_str()));
for (const auto& arg : args) {
argv.push_back(const_cast<char*>(arg.c_str()));
}
argv.push_back(nullptr);
execvp(command.c_str(), argv.data());
perror("execvp");
exit(-1);
} else {
if (use_pipe) {
close(pipefd[1]);
std::ostringstream oss;
char buf[4096];
ssize_t n;
while ((n = read(pipefd[0], buf, sizeof(buf))) > 0) {
oss.write(buf, n);
}
close(pipefd[0]);
*output = oss.str();
}
int status = 0;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
return WEXITSTATUS(status);
} else {
return -1;
}
}
}
} // anonymous namespace
int execute_cmd(
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,
sSSHInfo* sshinfo,
std::string* output
) {
if (sshinfo) {
std::string error;
ssh_session session = ssh_connect_and_auth(sshinfo, env, &error);
if (!session) {
if (output) *output = error;
return -1;
}
ssh_channel channel = ssh_channel_new(session);
if (!channel) {
if (output) *output = "Failed to create SSH channel.";
ssh_disconnect(session);
ssh_free(session);
return -1;
}
int rc = ssh_channel_open_session(channel);
if (rc != SSH_OK) {
if (output) *output = std::string("Failed to open SSH channel: ") + ssh_get_error(session);
ssh_channel_free(channel);
ssh_disconnect(session);
ssh_free(session);
return -1;
}
int ret = 0;
if (interactive) {
std::string remote_cmd_str = ssh_build_remote_command(command, args, working_dir, {});
ret = ssh_interactive_shell_session(session, channel, remote_cmd_str, command, output);
} else {
// For non-interactive, handle working directory in ssh_exec_command
std::string remote_cmd_str = ssh_build_command_only(command, args);
ret = ssh_exec_command(session, channel, remote_cmd_str, silent, output, env, working_dir);
}
ssh_channel_send_eof(channel);
ssh_channel_close(channel);
ssh_channel_free(channel);
ssh_disconnect(session);
ssh_free(session);
if (output) trim(output);
return ret;
} else {
int ret=local_execute_cmd(command, args, working_dir, env, silent, interactive, output);
if (output) trim(output);
return ret;
}
}
} // namespace runner

30
src/utils/runner.hpp Normal file
View File

@ -0,0 +1,30 @@
#ifndef RUNNER_HPP
#define RUNNER_HPP
#include <string>
#include <vector>
#include <map>
namespace runner {
struct sSSHInfo {
std::string host;
std::string user;
std::string port;
};
int execute_cmd(
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,
sSSHInfo* sshinfo = nullptr,
std::string* output = nullptr
);
} // namespace runner
#endif // RUNNER_HPP