Ctrl K
ComunidadGuías…

queue_lms_cli.h

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
#pragma once
#include <algorithm>
#include <arpa/inet.h>
#include <atomic>
#include <cctype>
#include <chrono>
#include <fcntl.h>
#include <netdb.h>
#include <poll.h>
#include <sstream>
#include <string>
#include <sys/socket.h>
#include <unistd.h>
#include <vector>
namespace queue_audio {
inline std::string decodeLms(const std::string &input) {
    auto hex = [](char c) -> int {
        if (c >= '0' && c <= '9')
            return c - '0';
        if (c >= 'a' && c <= 'f')
            return c - 'a' + 10;
        if (c >= 'A' && c <= 'F')
            return c - 'A' + 10;
        return -1;
    };
    std::string out;
    for (size_t i = 0; i < input.size(); ++i) {
        if (input[i] == '%' && i + 2 < input.size() && hex(input[i + 1]) >= 0 && hex(input[i + 2]) >= 0) {
            out += char(hex(input[i + 1]) * 16 + hex(input[i + 2]));
            i += 2;
        } else
            out += input[i];
    }
    return out;
}
// Bounded CLI adapter. Relay mutations must pass its speaker-command choke
// point. Raw MAC, exact decoded
// acknowledgement, fresh connection per bounded query.
inline bool controlLms(const std::string &host, const std::string &mac, const std::string &command,
                       unsigned port = 9090, std::string *reply = nullptr, unsigned timeoutMs = 0,
                       const std::atomic_bool *cancel = nullptr, bool global = false,
                       bool allowEmpty = false) {
    if (mac.size() != 17 || command.find_first_of("\r\n") != std::string::npos)
        return false;
    for (size_t i = 0; i < mac.size(); ++i)
        if (i % 3 == 2 ? mac[i] != ':' : !std::isxdigit(static_cast<unsigned char>(mac[i])))
            return false;
    addrinfo hints{}, *addresses = nullptr;
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    if (getaddrinfo(host.c_str(), std::to_string(port).c_str(), &hints, &addresses))
        return false;
    int fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
    if (fd < 0) {
        freeaddrinfo(addresses);
        return false;
    }
    connect(fd, addresses->ai_addr, addresses->ai_addrlen);
    freeaddrinfo(addresses);
    const auto deadline =
        std::chrono::steady_clock::now() +
        std::chrono::milliseconds(
            timeoutMs ? timeoutMs
                      : (command.rfind("status ", 0) == 0 || command.find("?") == std::string::npos ? 30000
                                                                                                    : 3000));
    const auto prefix = global ? std::string{} : mac + " ";
    const auto request = prefix + command + "\n";
    std::string response;
    size_t sent = 0;
    while (std::chrono::steady_clock::now() < deadline && (!cancel || !*cancel)) {
        pollfd p{fd, short(sent < request.size() ? POLLOUT : POLLIN), 0};
        if (poll(&p, 1, 20) <= 0)
            continue;
        if (sent < request.size()) {
            auto n = send(fd, request.data() + sent, request.size() - sent, MSG_NOSIGNAL);
            if (n <= 0)
                break;
            sent += n;
        } else {
            char bytes[1024];
            auto n = recv(fd, bytes, sizeof(bytes), 0);
            if (n <= 0)
                break;
            response.append(bytes, n);
            if (response.find('\n') != std::string::npos ||
                response.size() > (reply ? 16 * 1024 * 1024 : 4096))
                break;
        }
    }
    close(fd);
    auto end = response.find('\n');
    if (end == std::string::npos)
        return false;
    // Split before decoding: encoded spaces belong to a single argument.
    std::istringstream expectedInput(prefix + command), actualInput(response.substr(0, end));
    std::vector<std::string> expected, actual;
    std::string word;
    while (expectedInput >> word)
        expected.push_back(decodeLms(word));
    while (actualInput >> word)
        actual.push_back(decodeLms(word));
    const bool question = !expected.empty() && expected.back() == "?";
    if (question)
        expected.pop_back();
    const bool query =
        question || command.rfind("status ", 0) == 0 || command.rfind("podcasts items ", 0) == 0 ||
        (global && command.rfind("players ", 0) == 0);
    // Some clients encode the complete acknowledgement, including separators.
    // Accept only an exact decoded mutation echo in that representation.
    if (!query && actual.size() == 1 && actual.front() == decodeLms(prefix + command))
        actual = expected;
    if (actual.size() < expected.size() || !std::equal(expected.begin(), expected.end(), actual.begin()))
        return false;
    if (!query && actual.size() != expected.size())
        return false;
    for (size_t i = expected.size(); i < actual.size(); ++i)
        if (actual[i].rfind("error:", 0) == 0 || actual[i].rfind("badparams:", 0) == 0)
            return false;
    if (reply)
        *reply = response.substr(0, end);
    return !question || actual.size() > expected.size() || allowEmpty;
}
// Diagnostic-only client. One connection and one deadline cover all three
// queries; the caller runs this outside the audio clock and core event loop.
inline std::vector<std::string> pollLms(const std::string &host, unsigned port = 9090,
                                        std::string *raw = nullptr) {
    addrinfo hints{}, *addresses = nullptr;
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    if (getaddrinfo(host.c_str(), std::to_string(port).c_str(), &hints, &addresses))
        return {};
    int fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
    if (fd < 0) {
        freeaddrinfo(addresses);
        return {};
    }
    connect(fd, addresses->ai_addr, addresses->ai_addrlen);
    freeaddrinfo(addresses);
    const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(500);
    const std::string mac = "02:59:51:54:00:01";
    const std::vector<std::string> keys = {"playlist index", "time", "title"};
    std::string request;
    for (const auto &key : keys)
        request += mac + " " + key + " ?\n";
    size_t sent = 0;
    std::string response;
    while (std::chrono::steady_clock::now() < deadline) {
        pollfd p{fd, short(sent < request.size() ? POLLOUT : POLLIN), 0};
        if (poll(&p, 1, 20) <= 0)
            continue;
        if (sent < request.size()) {
            auto n = send(fd, request.data() + sent, request.size() - sent, MSG_NOSIGNAL);
            if (n <= 0)
                break;
            sent += n;
        } else {
            char bytes[4096];
            auto n = recv(fd, bytes, sizeof(bytes), 0);
            if (n <= 0)
                break;
            response.append(bytes, n);
            if (response.size() > 65536)
                break;
            if (std::count(response.begin(), response.end(), '\n') >= 3)
                break;
        }
    }
    close(fd);
    if (raw)
        *raw = response.substr(0, response.find('\n'));
    std::vector<std::string> values;
    size_t at = 0;
    for (const auto &key : keys) {
        auto end = response.find('\n', at);
        const auto line = decodeLms(response.substr(at, end == std::string::npos ? end : end - at));
        const auto prefix = mac + " " + key + " ";
        if (end == std::string::npos || line.compare(0, prefix.size(), prefix))
            return {};
        auto value = line.substr(prefix.size());
        if (!value.empty() && value.back() == '\r')
            value.pop_back();
        values.push_back(value);
        at = end + 1;
    }
    return values;
}
} // namespace queue_audio