diff --git a/source/src/commands/search.cpp b/source/src/commands/search.cpp new file mode 100644 index 0000000..1d65c24 --- /dev/null +++ b/source/src/commands/search.cpp @@ -0,0 +1,166 @@ +#include "command_registry.hpp" +#include "config.hpp" +#include "servers.hpp" +#include "services.hpp" +#include "shared_commands.hpp" +#include "tableprint.hpp" +#include "transwarp.hpp" +#include "utils/utils.hpp" + +#include +#include +#include + +namespace dropshell { + +int search_handler(const CommandContext& ctx); + +static std::vector search_name_list = {"search", "find"}; + +struct SearchCommandRegister { + SearchCommandRegister() { + CommandRegistry::instance().register_command({ + search_name_list, + search_handler, + nullptr, // no autocomplete + false, // hidden + true, // requires_config + true, // requires_install + 1, // min_args (search string required) + 1, // max_args + "search STRING", + "Search for services matching a pattern across all servers", + R"( +Search for services across all servers. Matches against both +service name and template name (case-insensitive substring match). + + search STRING Search all servers for matching services + )" + }); + } +} search_command_register; + +static bool icontains(const std::string& haystack, const std::string& needle) { + if (needle.empty()) return true; + auto it = std::search(haystack.begin(), haystack.end(), + needle.begin(), needle.end(), + [](char a, char b) { return std::tolower(a) == std::tolower(b); }); + return it != haystack.end(); +} + +int search_handler(const CommandContext& ctx) { + std::string search_term = ctx.args[0]; + + auto servers = get_configured_servers(); + if (servers.empty()) { + error << "No servers found" << std::endl; + return 1; + } + + // Build list of server/user pairs + typedef struct { ServerConfig server; UserConfig user; } server_user_pair; + std::vector server_user_pairs; + for (const auto& server : servers) + for (const auto& user : server.get_users()) + server_user_pairs.push_back({server, user}); + + // Collect matching results + struct SearchResult { + std::string server_name; + std::string service_name; + std::string template_name; + std::string health; + std::string ports; + }; + + std::vector results; + std::mutex results_mutex; + + info << "Searching " << server_user_pairs.size() << " agents: " << std::flush; + int checked = 0; + + transwarp::parallel exec{server_user_pairs.size()}; + auto task = transwarp::for_each(exec, server_user_pairs.begin(), server_user_pairs.end(), + [&](const server_user_pair& sup) { + std::string server_name = sup.server.get_server_name(); + + // Get local service info for name/template matching + auto services = get_server_services_info(server_name); + + // Filter to matching services first + std::vector matching; + for (const auto& svc : services) { + if (icontains(svc.service_name, search_term) || icontains(svc.template_name, search_term)) { + matching.push_back(svc); + } + } + + if (matching.empty()) { + std::lock_guard lock(results_mutex); + ++checked; + info << checked << " \xe2\x9c\x93 " << std::flush; + return; + } + + // Only query remote status if we have matches + sSSHInfo sshinfo = sup.server.get_SSH_INFO(sup.user.user); + bool is_online = execute_ssh_command(sshinfo, sCommand("", "true", {}), cMode::Silent); + + std::map status_map; + if (is_online) { + status_map = shared_commands::get_all_services_status(sup.server.get_server_name(), sup.user.user); + } + + { + std::lock_guard lock(results_mutex); + for (const auto& svc : matching) { + SearchResult r; + r.server_name = server_name; + r.service_name = svc.service_name; + r.template_name = svc.template_name; + + if (!is_online) { + r.health = ":cross:"; + r.ports = "-"; + } else { + auto it = status_map.find(svc.service_name); + if (it != status_map.end()) { + r.health = shared_commands::HealthStatus2String(it->second.health); + std::string ports_str; + for (const auto& port : it->second.ports) { + if (!ports_str.empty()) ports_str += " "; + ports_str += std::to_string(port); + } + r.ports = ports_str.empty() ? "-" : ports_str; + } else { + r.health = ":greytick:"; + r.ports = "-"; + } + } + + results.push_back(r); + } + ++checked; + info << checked << " \xe2\x9c\x93 " << std::flush; + } + }); + task->wait(); + info << std::endl << std::endl; + + if (results.empty()) { + info << "No services found matching: " << search_term << std::endl; + return 0; + } + + tableprint tp("Search results for: " + search_term); + tp.add_row({"Server", "Service", "Template", "Status", "Ports"}); + for (const auto& r : results) { + tp.add_row({r.server_name, r.service_name, r.template_name, r.health, r.ports}); + } + tp.sort({0, 1}); + tp.print(); + + return 0; +} + +} // namespace dropshell