feat: Add search command to find services across all servers
All checks were successful
Build-Test-Publish / build (linux/amd64) (push) Successful in 31s
Build-Test-Publish / build (linux/arm64) (push) Successful in 1m16s

This commit is contained in:
j
2026-03-07 18:53:54 +13:00
parent c165895ab4
commit 83a55bbed0

View File

@@ -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 <algorithm>
#include <mutex>
#include <iostream>
namespace dropshell {
int search_handler(const CommandContext& ctx);
static std::vector<std::string> 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_pair> 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<SearchResult> 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<LocalServiceInfo> 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<std::mutex> 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<std::string, shared_commands::ServiceStatus> status_map;
if (is_online) {
status_map = shared_commands::get_all_services_status(sup.server.get_server_name(), sup.user.user);
}
{
std::lock_guard<std::mutex> 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