#include "config.hpp" #include "service_runner.hpp" #include "server_env.hpp" #include "templates.hpp" #include "services.hpp" #include "utils/directories.hpp" #include "utils/utils.hpp" #include #include #include #include #include #include #include #include namespace fs = std::filesystem; namespace dropshell { service_runner::service_runner() : m_server_name(""), m_server_env(nullptr) {} bool service_runner::init(const std::string& server_name, const std::string& service_name) { if (server_name.empty() || service_name.empty()) return false; // Initialize server environment try { m_server_env = std::make_unique(server_name); if (!m_server_env->is_valid()) { std::cerr << "Error: Invalid server environment" << std::endl; m_server_env.reset(); return false; } } catch (const std::exception& e) { std::cerr << "Error: Failed to initialize server environment: " << e.what() << std::endl; return false; } m_server_name = server_name; m_service_info = get_service_info(server_name, service_name); mRemote_service_path = get_remote_service_path(m_server_name, m_service_info.service_name); mRemote_service_config_path = get_remote_service_config_path(m_server_name, m_service_info.service_name); mRemote_service_template_path = get_remote_service_template_path(m_server_name, m_service_info.service_name); mRemote_service_env_file = get_remote_service_env_file(m_server_name, m_service_info.service_name); return !m_service_info.path.empty(); } // Helper method implementations std::string service_runner::construct_ssh_cmd(const server_env &env) { std::stringstream ssh_cmd; ssh_cmd << "ssh -p " << env.get_SSH_PORT() << " " << env.get_SSH_USER() << "@" << env.get_SSH_HOST() << " "; return ssh_cmd.str(); } std::string service_runner::construct_ssh_cmd() const { if (!m_server_env) return std::string(); return construct_ssh_cmd(*m_server_env); } std::string service_runner::construct_standard_command_run_cmd(const std::string &command) const { std::string script_path = mRemote_service_template_path + "/" + command + ".sh"; std::string run_cmd = "'cd " + quote(mRemote_service_template_path) + " && /bin/bash "+quote(script_path)+" "+quote(mRemote_service_config_path)+"'"; return run_cmd; } bool service_runner::check_remote_dir_exists(const std::string &dir_path) const { std::string check_dir_cmd = construct_ssh_cmd() + "'test -d " + quote(dir_path) + "'"; if (system(check_dir_cmd.c_str()) != 0) { std::cerr << "Error: Directory not found on remote server:" << fs::path(dir_path).filename().string() << std::endl; return false; } return true; } bool service_runner::check_remote_file_exists(const std::string& file_path) const { std::string check_cmd = construct_ssh_cmd() + "'test -f " + quote(file_path) + "'"; if (system(check_cmd.c_str()) != 0) { std::cerr << "Error: File not found on remote server: " << fs::path(file_path).filename().string() << std::endl; return false; } return true; } bool service_runner::check_remote_items_exist(const std::vector &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 += fs::path(file_path).filename().string() + " "; } // check if all items in the vector exist on the remote server, in a single command. std::string check_cmd = construct_ssh_cmd() + "'for item in " + file_paths_str + "; do test -f $item; done'"; if (system(check_cmd.c_str()) != 0) { std::cerr << "Error: Required items not found on remote server: " << file_names_str << std::endl; return false; } return true; } bool service_runner::execute_ssh_command(const std::string& command, const std::string& error_msg) const { std::string full_cmd = construct_ssh_cmd() + command; return execute_local_command(full_cmd, error_msg); } bool service_runner::execute_local_command(const std::string& command, const std::string& error_msg) const { bool okay = (system(command.c_str()) == 0); if (!okay && !error_msg.empty()) std::cerr << "Error: " << error_msg << std::endl; return okay; } bool service_runner::execute_local_command_and_capture_output(const std::string &command, std::string &output) { std::string full_cmd = command + " 2>&1"; FILE *pipe = popen(full_cmd.c_str(), "r"); if (!pipe) { std::cerr << "Error: Failed to execute command: " << command << std::endl; return false; } char buffer[128]; while (fgets(buffer, sizeof(buffer), pipe) != nullptr) { output += buffer; } pclose(pipe); return true; } bool service_runner::install() { maketitle("Installing " + m_service_info.service_name + " (" + m_service_info.template_name + ") on " + m_server_name); if (!m_server_env) return false; // should never hit this. // Check if template exists template_info tinfo; if (!get_template_info(m_service_info.template_name, tinfo)) return false; // Create service directory std::string mkdir_cmd = "'mkdir -p " + quote(mRemote_service_path) + "'"; if (!execute_ssh_command(mkdir_cmd, "Failed to create service directory")) 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(check_rsync_cmd, "rsync is not installed on the remote host")) return false; // Copy template files { std::cout << "Copying template files from [LOCAL] " << tinfo.path << std::endl << std::string(24,' ')<<"to [REMOTE] " << mRemote_service_template_path << "/" << std::endl; std::string rsync_cmd = "rsync --delete -zrpc -e 'ssh -p " + m_server_env->get_SSH_PORT() + "' " + quote(tinfo.path + "/") + " "+ m_server_env->get_SSH_USER() + "@" + m_server_env->get_SSH_HOST() + ":" + quote(mRemote_service_template_path+"/"); //std::cout << std::endl << rsync_cmd << std::endl << std::endl; execute_local_command(rsync_cmd,"Failed to copy template files"); } // Copy service files (including service.env) { std::string local_service_path = get_local_service_path(m_server_name, m_service_info.service_name); if (local_service_path.empty() || !fs::exists(local_service_path)) { std::cerr << "Error: Service directory not found: " << local_service_path << std::endl; return false; } std::cout << "Copying service files from [LOCAL] " << local_service_path << std::endl <get_SSH_PORT() + "' " + quote(local_service_path + "/") + " "+ m_server_env->get_SSH_USER() + "@" + m_server_env->get_SSH_HOST() + ":" + quote(mRemote_service_config_path + "/"); execute_local_command(rsync_cmd,"Failed to copy service files"); } // Run install script { std::string install_cmd = construct_standard_command_run_cmd("install"); bool ok= execute_ssh_command(install_cmd, "Failed to run install script"); if (!ok) return false; } // print health tick std::cout << "Health: " << healthtick() << std::endl; return true; } bool service_runner::uninstall() { maketitle("Uninstalling " + m_service_info.service_name + " (" + m_service_info.template_name + ") on " + m_server_name); if (!m_server_env) return false; // should never hit this. // 2. Check if service directory exists on server if (!check_remote_dir_exists(mRemote_service_path)) { std::cerr << "Service is not installed: " << m_service_info.service_name << std::endl; return true; // Nothing to uninstall } // 3. Run uninstall script if it exists std::string uninstall_script = mRemote_service_template_path + "/_uninstall.sh"; bool script_exists = check_remote_file_exists(uninstall_script); if (script_exists) { std::string uninstall_cmd = construct_standard_command_run_cmd("uninstall"); if (!execute_ssh_command(uninstall_cmd, "Failed to run uninstall script")) { std::cerr << "Warning: Uninstall script failed, but continuing with directory removal" << std::endl; } } else { std::cerr << "Warning: No uninstall script found. Unable to uninstall service." << std::endl; return false; } // 4. Remove the service directory from the server std::string rm_cmd = "'rm -rf " + quote(mRemote_service_path) + "'"; if (!execute_ssh_command(rm_cmd, "Failed to remove service directory")) { return false; } std::cout << "Service " << m_service_info.service_name << " successfully uninstalled from " << m_server_name << std::endl; return true; } // ------------------------------------------------------------------------------------------------ // Run a command on the service. // ------------------------------------------------------------------------------------------------ bool service_runner::run_command(const std::string& command) { if (!m_server_env) { std::cerr << "Error: Server service not initialized" << std::endl; return false; } template_info tinfo; if (!get_template_info(m_service_info.template_name, tinfo)) { std::cerr << "Error: Template '" << m_service_info.template_name << "' not found" << std::endl; return false; } // don't need a script for edit! if (command == "edit") { edit_service_config(); return true; } if (!template_command_exists(m_service_info.template_name, command)) { std::cout << "No command script for " << m_service_info.template_name << " : " << command << std::endl; return true; // nothing to run. } // install doesn't require anything on the server yet. if (command == "install") return install(); std::string script_path = mRemote_service_template_path + "/" + command + ".sh"; // Check if service directory exists if (!check_remote_dir_exists(mRemote_service_path)) { return false; } // Check if command script exists if (!check_remote_file_exists(script_path)) { return false; } // Check if env file exists if (!check_remote_file_exists(mRemote_service_env_file)) { return false; } if (command == "uninstall") return uninstall(); if (command == "ssh") { interactive_ssh_service(); return true; } // Run the generic command std::string run_cmd = construct_standard_command_run_cmd(command); return execute_ssh_command(run_cmd, "Command returned error code: " + script_path); } std::map service_runner::get_all_services_status(std::string server_name) { std::map status; std::string command = "_allservicesstatus"; std::string service_name = "dropshell-agent"; if (!template_command_exists(service_name, command)) { std::cerr << "Error: " << service_name << " does not contain the " << command << " script" << std::endl; return status; } std::string remote_service_path = get_remote_service_path(server_name, service_name); std::string remote_service_config_path = get_remote_service_config_path(server_name, service_name); std::string remote_service_template_path = get_remote_service_template_path(server_name, service_name); std::string remote_service_env_file = get_remote_service_env_file(server_name, service_name); server_env env(server_name); if (!env.is_valid()) { std::cerr << "Error: Invalid server environment" << std::endl; return status; } std::string script_path = remote_service_template_path + "/" + command + ".sh"; std::string ssh_cmd = construct_ssh_cmd(env) + "'cd " + quote(remote_service_template_path) + " && /bin/bash "+quote(script_path)+" "+quote(remote_service_config_path)+"'"; std::string output; if (!execute_local_command_and_capture_output(ssh_cmd, output)) return status; std::stringstream ss(output); std::string line; while (std::getline(ss, line)) { std::string key, value; std::size_t pos = line.find("="); if (pos != std::string::npos) { key = dequote(trim(line.substr(0, pos))); value = dequote(trim(line.substr(pos + 1))); // decode key, it's of format SERVICENAME_[HEALTH|PORTS] std::string service_name = key.substr(0, key.find_last_of("_")); std::string status_type = key.substr(key.find_last_of("_") + 1); if (status_type == "HEALTH") { // healthy|unhealthy|unknown if (value == "healthy") status[service_name].health = HealthStatus::HEALTHY; else if (value == "unhealthy") status[service_name].health = HealthStatus::UNHEALTHY; else if (value == "unknown") status[service_name].health = HealthStatus::UNKNOWN; else status[service_name].health = HealthStatus::ERROR; } else if (status_type == "PORTS") { // port1,port2,port3 std::vector ports = string2multi(value); for (const auto& port : ports) { if (port!="unknown") status[service_name].ports.push_back(str2int(port)); } } } } return status; } HealthStatus service_runner::is_healthy() { if (!m_server_env) { std::cerr << "Error: Server service not initialized" << std::endl; return HealthStatus::ERROR; } // Check if status script exists std::string command = "status"; if (!template_command_exists(m_service_info.template_name, command)) { return HealthStatus::UNKNOWN; } std::string script_path = mRemote_service_template_path + "/" + command + ".sh"; if (!check_remote_file_exists(script_path)) { std::cerr << "Service is not installed: " << m_service_info.service_name << std::endl; return HealthStatus::NOTINSTALLED; } // Run status script, does not display output. std::string run_cmd = construct_standard_command_run_cmd("status"); bool ok = execute_ssh_command(run_cmd, ""); if (!ok) return HealthStatus::UNHEALTHY; return HealthStatus::HEALTHY; } std::string service_runner::healthtick() { std::string green_tick = "\033[32m✓\033[0m"; std::string red_cross = "\033[31m✗\033[0m"; std::string yellow_exclamation = "\033[33m!\033[0m"; std::string unknown = "\033[33m?\033[0m"; HealthStatus status = is_healthy(); if (status == HealthStatus::HEALTHY) return green_tick; else if (status == HealthStatus::UNHEALTHY) return red_cross; else if (status == HealthStatus::UNKNOWN) return unknown; else return yellow_exclamation; } std::string service_runner::HealthStatus2String(HealthStatus status) { if (status == HealthStatus::HEALTHY) return ":tick:"; else if (status == HealthStatus::UNHEALTHY) return ":cross:"; else if (status == HealthStatus::UNKNOWN) return ":question:"; else if (status == HealthStatus::NOTINSTALLED) return ":warning:"; else return ":error:"; } std::string service_runner::healthmark() { HealthStatus status = is_healthy(); return HealthStatus2String(status); } void interactive_ssh(const std::string & server_name, const std::string & command) { std::string serverpath = get_local_server_path(server_name); if (serverpath.empty()) { std::cerr << "Error: Server not found: " << server_name << std::endl; return; } server_env env(server_name); if (!env.is_valid()) { std::cerr << "Error: Invalid server environment file: " << server_name << std::endl; return; } std::string ssh_address = env.get_SSH_HOST(); std::string ssh_user = env.get_SSH_USER(); std::string ssh_port = env.get_SSH_PORT(); std::string login = ssh_user + "@" + ssh_address; // Execute ssh with server_name and command if (command.empty()) execlp("ssh", "ssh", "-tt", login.c_str(), "-p", ssh_port.c_str(), nullptr); else execlp("ssh", "ssh", "-tt", login.c_str(), "-p", ssh_port.c_str(), command.c_str(), nullptr); // If exec returns, it means there was an error perror("ssh execution failed"); exit(EXIT_FAILURE); } void edit_server(const std::string &server_name) { std::string serverpath = get_local_server_path(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 "< used_commands = get_used_commands(m_server_name, m_service_info.service_name); if (used_commands.find("ssh") == used_commands.end()) { std::cerr << "Error: "<< m_service_info.service_name <<" does not support ssh" << std::endl; return; } std::string command = construct_standard_command_run_cmd("ssh"); interactive_ssh(m_server_name, "/bin/bash -c '"+command+"'"); } void service_runner::edit_service_config() { std::string config_file = get_local_service_env_path(m_server_name, m_service_info.service_name); if (!fs::exists(config_file)) { std::cerr << "Error: Service config file not found: " << config_file << std::endl; return; } std::string aftertext = "To apply your changes, run:\n dropshell install " + m_server_name + " " + m_service_info.service_name; edit_file(config_file, aftertext); } } // namespace dropshell