Implementing commands
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 20s

This commit is contained in:
Your Name 2025-05-11 12:22:36 +12:00
parent 3c8a66c241
commit 78dbf4aff3
11 changed files with 341 additions and 111 deletions

View File

@ -44,6 +44,7 @@ target_include_directories(dropshell PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_SOURCE_DIR}/src/utils
${CMAKE_CURRENT_SOURCE_DIR}/src/contrib
${CMAKE_CURRENT_SOURCE_DIR}/src/commands
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/src>
)

45
src/command_registry.cpp Normal file
View File

@ -0,0 +1,45 @@
#include "command_registry.hpp"
CommandRegistry& CommandRegistry::instance() {
static CommandRegistry reg;
return reg;
}
void CommandRegistry::register_command(const CommandInfo& info) {
auto ptr = std::make_shared<CommandInfo>(info);
for (const auto& name : info.names) {
command_map_[name] = ptr;
}
all_commands_.push_back(ptr);
}
const CommandInfo* CommandRegistry::find_command(const std::string& name) const {
auto it = command_map_.find(name);
if (it != command_map_.end()) return it->second.get();
return nullptr;
}
std::vector<std::string> CommandRegistry::list_commands(bool include_hidden) const {
std::set<std::string> out;
for (const auto& cmd : all_commands_) {
if (!cmd->hidden || include_hidden) {
for (const auto& name : cmd->names) out.insert(name);
}
}
return std::vector<std::string>(out.begin(), out.end());
}
void CommandRegistry::autocomplete(const std::vector<std::string>& args) const {
if (args.size() < 3) {
for (const auto& name : list_commands(false)) {
std::cout << name << std::endl;
}
return;
}
std::string cmd = args[2];
auto* info = find_command(cmd);
if (info && info->autocomplete) {
CommandContext ctx{args};
info->autocomplete(ctx);
}
}

46
src/command_registry.hpp Normal file
View File

@ -0,0 +1,46 @@
#pragma once
#include <string>
#include <vector>
#include <functional>
#include <map>
#include <set>
#include <memory>
#include <iostream>
struct CommandContext {
std::vector<std::string> args;
// Add more fields as needed (e.g., config pointer, output stream, etc.)
};
struct CommandInfo {
std::vector<std::string> names;
std::function<int(const CommandContext&)> handler;
std::function<void(const CommandContext&)> autocomplete; // optional
bool hidden = false;
bool requires_config = false;
int min_args = 0;
int max_args = -1; // -1 = unlimited
std::string help_usage; // install SERVER [SERVICE]
std::string help_description; // Install/reinstall/update service(s). Safe/non-destructive.
};
class CommandRegistry {
public:
static CommandRegistry& instance();
void register_command(const CommandInfo& info);
// Returns nullptr if not found
const CommandInfo* find_command(const std::string& name) const;
// List all commands (optionally including hidden)
std::vector<std::string> list_commands(bool include_hidden = false) const;
// For autocomplete
void autocomplete(const std::vector<std::string>& args) const;
private:
CommandRegistry() = default;
std::map<std::string, std::shared_ptr<CommandInfo>> command_map_;
std::vector<std::shared_ptr<CommandInfo>> all_commands_;
};

169
src/commands/edit.cpp Normal file
View File

@ -0,0 +1,169 @@
#include "command_registry.hpp"
#include "config.hpp"
#include "utils/utils.hpp"
#include "service_runner.hpp"
#include "utils/directories.hpp"
#include "standard_autocomplete.hpp"
#include <unistd.h>
#include <cstring>
#include <iostream>
#include <sstream>
#include <filesystem>
#include <libassert/assert.hpp>
namespace dropshell {
void edit_autocomplete(const CommandContext& ctx);
int edit_handler(const CommandContext& ctx);
static std::vector<std::string> edit_name_list={"edit"};
// Static registration
struct EditCommandRegister {
EditCommandRegister() {
CommandRegistry::instance().register_command({
edit_name_list,
edit_handler,
edit_autocomplete,
false, // hidden
false, // requires_config
0, // min_args (after command)
2 // max_args (after command)
});
}
} edit_command_register;
// ------------------------------------------------------------------------------------------------
// edit command implementation
// ------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------
// utility function to edit a file
// ------------------------------------------------------------------------------------------------
bool edit_file(const std::string &file_path)
{
// make sure parent directory exists.
std::string parent_dir = get_parent(file_path);
std::filesystem::create_directories(parent_dir);
std::string editor_cmd;
const char* editor_env = std::getenv("EDITOR");
if (editor_env && std::strlen(editor_env) > 0) {
editor_cmd = std::string(editor_env) + " " + quote(file_path);
} else if (isatty(STDIN_FILENO)) {
// Check if stdin is connected to a terminal if EDITOR is not set
editor_cmd = "nano -w " + quote(file_path);
} else {
std::cerr << "Error: Standard input is not a terminal and EDITOR environment variable is not set." << std::endl;
std::cerr << "Try setting the EDITOR environment variable (e.g., export EDITOR=nano) or run in an interactive terminal." << std::endl;
std::cerr << "You can manually edit the file at: " << file_path << std::endl;
return false;
}
std::cout << "Editing file: " << file_path << std::endl;
return execute_local_command(editor_cmd, nullptr, cMode::Interactive | cMode::RawCommand);
}
// ------------------------------------------------------------------------------------------------
// edit config
// ------------------------------------------------------------------------------------------------
int edit_config()
{
if (!gConfig().is_config_set())
gConfig().save_config(false); // save defaults.
std::string config_file = localfile::dropshell_json();
if (!edit_file(config_file) || !std::filesystem::exists(config_file))
return die("Error: Failed to edit config file.");
gConfig().load_config();
if (!gConfig().is_config_set())
return die("Error: Failed to load and parse edited config file!");
gConfig().save_config(true);
std::cout << "Successfully edited config file at " << config_file << std::endl;
return 0;
}
// ------------------------------------------------------------------------------------------------
// edit server
// ------------------------------------------------------------------------------------------------
int edit_server(const std::string &server_name)
{
std::string serverpath = localpath::server(server_name);
if (serverpath.empty()) {
std::cerr << "Error: Server not found: " << server_name << std::endl;
return -1;
}
std::ostringstream aftertext;
aftertext << "If you have changed DROPSHELL_DIR, you should manually move the files to the new location NOW.\n"
<< "You can ssh in to the remote server with: dropshell ssh "<<server_name<<"\n"
<< "Once moved, reinstall all services with: dropshell install " << server_name;
std::string config_file = serverpath + "/server.env";
if (!edit_file(config_file)) {
std::cerr << "Error: Failed to edit server.env" << std::endl;
std::cerr << "You can manually edit this file at: " << config_file << std::endl;
std::cerr << "After editing, " << aftertext.str() << std::endl;
}
else
std::cout << aftertext.str() << std::endl;
return 0;
}
// ------------------------------------------------------------------------------------------------
// edit service config
// ------------------------------------------------------------------------------------------------
int edit_service_config(const std::string &server, const std::string &service)
{
std::string config_file = localfile::service_env(server, service);
if (!std::filesystem::exists(config_file))
{
std::cerr << "Error: Service config file not found: " << config_file << std::endl;
return 1;
}
if (edit_file(config_file) && std::filesystem::exists(config_file))
std::cout << "To apply your changes, run:\n dropshell install " + server + " " + service << std::endl;
return 0;
}
// ------------------------------------------------------------------------------------------------
// edit command handler
// ------------------------------------------------------------------------------------------------
int edit_handler(const CommandContext& ctx) {
// edit dropshell config
if (ctx.args.size() < 3)
return edit_config();
// edit server config
if (ctx.args.size() < 4) {
edit_server(safearg(ctx.args,2));
return 0;
}
// edit service config
if (ctx.args.size() < 5) {
edit_service_config(safearg(ctx.args,2), safearg(ctx.args,3));
return 0;
}
std::cout << "Edit handler called with " << ctx.args.size() << " args\n";
return -1;
}
// ------------------------------------------------------------------------------------------------
// edit command autocomplete
// ------------------------------------------------------------------------------------------------
void edit_autocomplete(const CommandContext& ctx) {
std_autocomplete(edit_name_list, ctx);
}
} // namespace dropshell

View File

@ -0,0 +1,27 @@
#include "standard_autocomplete.hpp"
#include "servers.hpp"
#include "services.hpp"
void dropshell::std_autocomplete(const std::vector<std::string> &name_list, const CommandContext &ctx)
{
if (ctx.args.size() == 1) {
// edit command
for (const auto& name : name_list) {
std::cout << name << std::endl;
}
}
else if (ctx.args.size() == 2) {
// list servers
std::vector<ServerInfo> servers = get_configured_servers();
for (const auto& server : servers) {
std::cout << server.name << std::endl;
}
}
else if (ctx.args.size() == 3) {
// list services
std::vector<LocalServiceInfo> services = get_server_services_info(ctx.args[2]);
for (const auto& service : services) {
std::cout << service.service_name << std::endl;
}
}
}

View File

@ -0,0 +1,15 @@
#ifndef STANDARD_AUTOCOMPLETE_HPP
#define STANDARD_AUTOCOMPLETE_HPP
#include <vector>
#include <string>
#include "command_registry.hpp"
namespace dropshell {
void std_autocomplete(const std::vector<std::string>& name_list, const CommandContext& ctx);
} // namespace dropshell
#endif

View File

@ -8,6 +8,7 @@
#include "utils/utils.hpp"
#include "autocomplete.hpp"
#include "utils/hash.hpp"
#include "command_registry.hpp"
#include <filesystem>
#include <iostream>
@ -17,7 +18,7 @@
#include <chrono>
#include <libassert/assert.hpp>
#include <sstream>
#include <algorithm>
namespace dropshell {
extern const std::string VERSION;
@ -25,6 +26,15 @@ extern const std::string RELEASE_DATE;
extern const std::string AUTHOR;
extern const std::string LICENSE;
void show_command(const std::string& cmd) {
const auto& cmd_info = CommandRegistry::instance().find_command(cmd);
if (cmd_info) {
std::cout << " " << cmd_info->help_usage
<< std::string(' ', std::min(1,(int)(30-cmd_info->help_usage.length())))
<< cmd_info->help_description << std::endl;
}
}
bool print_help() {
std::cout << std::endl;
maketitle("DropShell version " + VERSION);
@ -32,11 +42,10 @@ bool print_help() {
std::cout << "A tool for managing server configurations" << std::endl;
std::cout << std::endl;
std::cout << "dropshell ..." << std::endl;
std::cout << " help Show this help message" << std::endl;
std::cout << " edit Edit the configuration of dropshell" << std::endl;
show_command("help");
show_command("edit");
if (gConfig().is_config_set()) {
std::cout << " server NAME Show details for specific server" << std::endl;
std::cout << " templates List all available templates" << std::endl;
std::cout << std::endl;
std::cout << std::endl;
@ -59,19 +68,14 @@ bool print_help() {
std::cout << " create-service SERVER TEMPLATE SERVICE" << std::endl;
}
else {
std::cout << " edit Edit the configuration of dropshell" << std::endl;
show_command("help");
show_command("edit");
std::cout << std::endl;
std::cout << "Other commands available once initialised." << std::endl;
}
return true;
}
int die(const std::string & msg) {
std::cerr << msg << std::endl;
return 1;
}
struct ServerAndServices {
std::string server_name;
std::vector<LocalServiceInfo> servicelist;
@ -91,12 +95,6 @@ bool getCLIServices(const std::string & arg2, const std::string & arg3,
return true;
}
std::string safearg(int argc, char *argv[], int index)
{
if (index >= argc) return "";
return argv[index];
}
void printversion() {
maketitle("DropShell version " + VERSION);
std::cout << "Release date: " << RELEASE_DATE << std::endl;
@ -128,23 +126,6 @@ auto command_match = [](const std::string& cmd_list, int argc, char* argv[]) ->
} \
}
int edit_config()
{
if (!gConfig().is_config_set())
gConfig().save_config(false); // save defaults.
std::string config_file = localfile::dropshell_json();
if (!service_runner::edit_file(config_file) || !std::filesystem::exists(config_file))
return die("Error: Failed to edit config file.");
gConfig().load_config();
if (!gConfig().is_config_set())
return die("Error: Failed to load and parse edited config file!");
gConfig().save_config(true);
std::cout << "Successfully edited config file at " << config_file << std::endl;
return 0;
}
int main(int argc, char* argv[]) {
HAPPYEXIT("hash", hash_demo_raw(safearg(argc,argv,2)))
@ -173,8 +154,6 @@ int main(int argc, char* argv[]) {
return autocomplete::autocomplete(argvec) ? 0 : 1;
}
if (cmd == "edit" && argc < 3)
return edit_config();
// ------------------------------------------------------------
// from here we require the config file to be loaded.
@ -231,11 +210,6 @@ int main(int argc, char* argv[]) {
return 0;
}
if (cmd == "edit" && argc < 4) {
ASSERT(argc>=3, "Error: logic error!");
service_runner::edit_server(safearg(argc,argv,2));
return 0;
}
// handle running a command.
std::set<std::string> commands;

View File

@ -194,12 +194,6 @@ bool service_runner::run_command(const std::string& command, std::vector<std::st
return false;
}
// don't need a script for edit!
if (command == "edit") {
edit_service_config();
return true;
}
if (command == "fullnuke")
return fullnuke();
@ -397,53 +391,7 @@ bool service_runner::interactive_ssh(const std::string & server_name, const std:
return execute_ssh_command(env.get_SSH_INFO(), scommand, cMode::Interactive | cMode::RawCommand);
}
void service_runner::edit_server(const std::string &server_name)
{
std::string serverpath = localpath::server(server_name);
if (serverpath.empty()) {
std::cerr << "Error: Server not found: " << server_name << std::endl;
return;
}
std::ostringstream aftertext;
aftertext << "If you have changed DROPSHELL_DIR, you should manually move the files to the new location NOW.\n"
<< "You can ssh in to the remote server with: dropshell ssh "<<server_name<<"\n"
<< "Once moved, reinstall all services with: dropshell install " << server_name;
std::string config_file = serverpath + "/server.env";
if (!edit_file(config_file)) {
std::cerr << "Error: Failed to edit server.env" << std::endl;
std::cerr << "You can manually edit this file at: " << config_file << std::endl;
std::cerr << "After editing, " << aftertext.str() << std::endl;
}
else
std::cout << aftertext.str() << std::endl;
}
bool service_runner::edit_file(const std::string &file_path)
{
// make sure parent directory exists.
std::string parent_dir = get_parent(file_path);
std::filesystem::create_directories(parent_dir);
std::string editor_cmd;
const char* editor_env = std::getenv("EDITOR");
if (editor_env && std::strlen(editor_env) > 0) {
editor_cmd = std::string(editor_env) + " " + quote(file_path);
} else if (isatty(STDIN_FILENO)) {
// Check if stdin is connected to a terminal if EDITOR is not set
editor_cmd = "nano -w " + quote(file_path);
} else {
std::cerr << "Error: Standard input is not a terminal and EDITOR environment variable is not set." << std::endl;
std::cerr << "Try setting the EDITOR environment variable (e.g., export EDITOR=nano) or run in an interactive terminal." << std::endl;
std::cerr << "You can manually edit the file at: " << file_path << std::endl;
return false;
}
std::cout << "Editing file: " << file_path << std::endl;
return execute_local_command(editor_cmd, nullptr, cMode::Interactive | cMode::RawCommand);
}
bool service_runner::interactive_ssh_service()
{
@ -457,18 +405,6 @@ bool service_runner::interactive_ssh_service()
return mServerEnv.run_remote_template_command(mService, "ssh", args, false, {});
}
void service_runner::edit_service_config()
{
std::string config_file = localfile::service_env(mServer,mService);
if (!fs::exists(config_file)) {
std::cerr << "Error: Service config file not found: " << config_file << std::endl;
return;
}
if (edit_file(config_file) && std::filesystem::exists(config_file))
std::cout << "To apply your changes, run:\n dropshell install " + mServer + " " + mService << std::endl;
}
bool service_runner::scp_file_to_remote(const std::string &local_path, const std::string &remote_path, bool silent)
{
std::string scp_cmd = "scp -P " + mServerEnv.get_SSH_PORT() + " " + quote(local_path) + " " + mServerEnv.get_SSH_USER() + "@" + mServerEnv.get_SSH_HOST() + ":" + quote(remote_path) + (silent ? " > /dev/null 2>&1" : "");

View File

@ -55,7 +55,7 @@ class service_runner {
std::string healthtick();
std::string healthmark();
private:
public:
// install the service over ssh, using the credentials from server.env (via server_env.hpp), by:
// 1. check if the server_name exists, and the service_name refers to a valid template
// 2. check if service_name is valid for the server_name
@ -84,9 +84,6 @@ class service_runner {
// replaces the current dropshell process with the ssh process
bool interactive_ssh_service();
// edit the service configuration file
void edit_service_config();
bool rsync_tree_to_remote(const std::string& local_path, const std::string& remote_path, bool silent=false);
bool scp_file_to_remote(const std::string& local_path, const std::string& remote_path, bool silent=false);
bool scp_file_from_remote(const std::string& remote_path, const std::string& local_path, bool silent=false);
@ -94,8 +91,6 @@ class service_runner {
// utility functions
static std::string get_latest_backup_file(const std::string& server, const std::string& service);
static bool interactive_ssh(const std::string & server_name, const std::string & command);
static void edit_server(const std::string & server_name);
static bool edit_file(const std::string & file_path);
static std::map<std::string, ServiceStatus> get_all_services_status(std::string server_name);
static std::string HealthStatus2String(HealthStatus status);

View File

@ -316,4 +316,22 @@ std::string requote(std::string str) {
return quote(trim(dequote(trim(str))));
}
int die(const std::string & msg) {
std::cerr << msg << std::endl;
return 1;
}
std::string safearg(const std::vector<std::string> & args, int index)
{
if (index >= args.size()) return "";
return args[index];
}
std::string safearg(int argc, char *argv[], int index)
{
if (index >= argc) return "";
return argv[index];
}
} // namespace dropshell

View File

@ -41,4 +41,8 @@ std::string replace_with_environment_variables_like_bash(std::string str);
std::string random_alphanumeric_string(int length);
int die(const std::string & msg);
std::string safearg(int argc, char *argv[], int index);
std::string safearg(const std::vector<std::string> & args, int index);
} // namespace dropshell