exec command, and remote execution improvements!
All checks were successful
Build-Test-Publish / build (linux/amd64) (push) Successful in 37s
Build-Test-Publish / build (linux/arm64) (push) Successful in 1m6s

This commit is contained in:
Your Name
2025-09-13 07:28:00 +12:00
parent 6542590942
commit 117af635a3
3 changed files with 276 additions and 2 deletions

View File

@@ -0,0 +1,137 @@
#include "command_registry.hpp"
#include "config.hpp"
#include "utils/utils.hpp"
#include "utils/directories.hpp"
#include "utils/execute.hpp"
#include "shared_commands.hpp"
#include "servers.hpp"
#include "services.hpp"
#include "templates.hpp"
#include "utils/output.hpp"
#include <libassert/assert.hpp>
namespace dropshell
{
int exec_handler(const CommandContext &ctx);
static std::vector<std::string> exec_name_list = {"exec"};
// Static registration
struct ExecCommandRegister
{
ExecCommandRegister()
{
CommandRegistry::instance().register_command({exec_name_list,
exec_handler,
shared_commands::std_autocomplete,
false, // hidden
true, // requires_config
true, // requires_install
3, // min_args (server, service, command)
-1, // max_args (unlimited for command with args)
"exec SERVER SERVICE COMMAND [ARGS...]",
"Execute a command on a server within a service environment.",
R"(
exec SERVER SERVICE COMMAND [ARGS...] Execute a command in the service's environment.
This command runs a command on the remote server with the service's environment
variables loaded (including those from service.env and the usual variables).
The command is executed in the service's template directory context.
)"});
}
} exec_command_register;
int exec_handler(const CommandContext &ctx)
{
if (ctx.args.size() < 3)
{
error << "Server name, service name, and command are required" << std::endl;
return 1;
}
std::string server = safearg(ctx.args, 0);
std::string service = safearg(ctx.args, 1);
std::string command = safearg(ctx.args, 2);
// Collect any additional arguments for the command
std::vector<std::string> command_args;
for (size_t i = 3; i < ctx.args.size(); ++i)
{
command_args.push_back(ctx.args[i]);
}
// Validate server
ServerConfig server_env(server);
if (!server_env.is_valid())
{
error << "Server " << server << " is not valid" << std::endl;
return 1;
}
// Validate service name
if (!legal_service_name(service))
{
error << "Service name contains illegal characters: " << service << std::endl;
return 1;
}
// Get service info to validate it exists
LocalServiceInfo sinfo = get_service_info(server, service);
if (!SIvalid(sinfo))
{
error << "Service " << service << " is not valid on server " << server << std::endl;
return 1;
}
// Get the user for this service
std::string user = server_env.get_user_for_service(service);
// Get all service environment variables
std::map<std::string, std::string> env_vars;
if (!get_all_service_env_vars(server, service, env_vars))
{
error << "Failed to get environment variables for service " << service << std::endl;
return 1;
}
// Add HOST_NAME like other commands do
env_vars["HOST_NAME"] = server_env.get_SSH_HOST();
// Get the remote service template path for working directory
std::string remote_service_template_path = remotepath(server, user).service_template(service);
// Build the command string with arguments
std::string full_command = command;
for (const auto &arg : command_args)
{
full_command += " " + quote(dequote(trim(arg)));
}
// Create the command structure with environment variables
// Note: execute_ssh_command will automatically use bb64 to encode and execute this safely
sCommand scommand(remote_service_template_path, full_command, env_vars);
// Execute the command on the remote server
info << "Executing command on " << server << "/" << service << ": " << command;
if (!command_args.empty())
{
rawout << " with args:";
for (const auto &arg : command_args)
rawout << " " << arg;
}
rawout << std::endl;
bool success = execute_ssh_command(server_env.get_SSH_INFO(user), scommand, cMode::Interactive);
if (!success)
{
error << "Command execution failed" << std::endl;
return 1;
}
return 0;
}
} // namespace dropshell

View File

@@ -6,6 +6,7 @@
#include <string>
#include <cstdlib>
#include <sstream>
#include <cctype>
#include <libassert/assert.hpp>
#include "execute.hpp"
@@ -203,6 +204,28 @@ namespace dropshell
return commandstr;
}
// ----------------------------------------------------------------------------------------------------------
// sanitize_env_var_name - Basic sanity check for environment variable names
// ----------------------------------------------------------------------------------------------------------
static bool is_valid_env_var_name(const std::string &name)
{
if (name.empty())
return false;
// Must start with letter or underscore
if (!std::isalpha(name[0]) && name[0] != '_')
return false;
// Rest must be alphanumeric or underscore
for (char c : name)
{
if (!std::isalnum(c) && c != '_')
return false;
}
return true;
}
// ----------------------------------------------------------------------------------------------------------
// construct_cmd
// ----------------------------------------------------------------------------------------------------------
@@ -220,8 +243,29 @@ namespace dropshell
cmdstr += "cd " + quote(mDir) + " && ";
if (!mVars.empty())
{
// Export variables so they're available for expansion in the command
for (const auto &env_var : mVars)
cmdstr += env_var.first + "=" + quote(dequote(trim(env_var.second))) + " ";
{
// Basic sanity check - skip invalid variable names
if (!is_valid_env_var_name(env_var.first))
{
error << "Skipping invalid environment variable name: " << env_var.first << std::endl;
continue;
}
// Very basic check for completely broken values that could break the command
// We still use quote() for proper escaping, but warn about suspicious values
const std::string &value = env_var.second;
if (value.find('\0') != std::string::npos)
{
error << "Skipping environment variable with null byte: " << env_var.first << std::endl;
continue;
}
cmdstr += "export " + env_var.first + "=" + quote(dequote(trim(value))) + " && ";
}
}
cmdstr += mCmd;