Files
dynagate-cpp/config.cpp
T
2026-10-14 09:18:19 +03:00

577 lines
15 KiB
C++

/*
Dynagate config manager, CSV parser, and utilities (C++20 port)
Created by Luxferre in 2026, released into the public domain
*/
#ifndef _DARWIN_C_SOURCE
#define _DARWIN_C_SOURCE
#endif
#include "dynagate.hpp"
#include <algorithm>
#include <cctype>
#include <cerrno>
#include <cstdarg>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <fstream>
#include <sstream>
#include <thread>
#include <unordered_set>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
namespace {
std::mutex g_log_mu;
std::string run_shell_command(const std::string &command, int &exit_status) {
exit_status = -1;
int pipefd[2];
if (pipe(pipefd) != 0) {
return "pipe() failed: " + std::string(std::strerror(errno));
}
pid_t pid = fork();
if (pid < 0) {
close(pipefd[0]);
close(pipefd[1]);
return "fork() failed: " + std::string(std::strerror(errno));
}
if (pid == 0) {
close(pipefd[0]);
dup2(pipefd[1], STDOUT_FILENO);
dup2(pipefd[1], STDERR_FILENO);
close(pipefd[1]);
execl("/bin/sh", "sh", "-c", command.c_str(), static_cast<char *>(nullptr));
_exit(127);
}
close(pipefd[1]);
std::string output;
char buf[4096];
for (;;) {
ssize_t n = read(pipefd[0], buf, sizeof(buf));
if (n > 0) {
output.append(buf, static_cast<size_t>(n));
} else if (n == 0) {
break;
} else if (errno != EINTR) {
break;
}
}
close(pipefd[0]);
int status = 0;
while (waitpid(pid, &status, 0) < 0) {
if (errno != EINTR) {
break;
}
}
if (WIFEXITED(status)) {
exit_status = WEXITSTATUS(status);
} else if (WIFSIGNALED(status)) {
exit_status = 128 + WTERMSIG(status);
}
return output;
}
} // namespace
void log_msg(const char *fmt, ...) {
std::lock_guard<std::mutex> lock(g_log_mu);
std::time_t now = std::time(nullptr);
struct tm tm_now;
localtime_r(&now, &tm_now);
char time_buf[32];
std::strftime(time_buf, sizeof(time_buf), "%Y/%m/%d %H:%M:%S", &tm_now);
char msg_buf[2048];
va_list ap;
va_start(ap, fmt);
std::vsnprintf(msg_buf, sizeof(msg_buf), fmt, ap);
va_end(ap);
std::fprintf(stderr, "%s %s\n", time_buf, msg_buf);
std::fflush(stderr);
}
std::string str_trim(std::string_view sv) {
size_t start = 0;
while (start < sv.size() &&
std::isspace(static_cast<unsigned char>(sv[start]))) {
++start;
}
size_t end = sv.size();
while (end > start &&
std::isspace(static_cast<unsigned char>(sv[end - 1]))) {
--end;
}
return std::string(sv.substr(start, end - start));
}
std::string str_to_lower(std::string_view sv) {
std::string out(sv);
std::transform(out.begin(), out.end(), out.begin(), [](unsigned char c) {
return static_cast<char>(std::tolower(c));
});
return out;
}
bool str_iequals(std::string_view a, std::string_view b) {
if (a.size() != b.size()) {
return false;
}
for (size_t i = 0; i < a.size(); ++i) {
if (std::tolower(static_cast<unsigned char>(a[i])) !=
std::tolower(static_cast<unsigned char>(b[i]))) {
return false;
}
}
return true;
}
void HeaderList::add(std::string_view key, std::string_view value) {
items.emplace_back(std::string(key), std::string(value));
}
void HeaderList::set(std::string_view key, std::string_view value) {
del(key);
items.emplace_back(std::string(key), std::string(value));
}
void HeaderList::del(std::string_view key) {
items.erase(
std::remove_if(items.begin(), items.end(), [&](const auto &kv) {
return str_iequals(kv.first, key);
}),
items.end());
}
std::string HeaderList::get(std::string_view key) const {
for (const auto &kv : items) {
if (str_iequals(kv.first, key)) {
return kv.second;
}
}
return "";
}
std::vector<std::string> HeaderList::values(std::string_view key) const {
std::vector<std::string> res;
for (const auto &kv : items) {
if (str_iequals(kv.first, key)) {
res.push_back(kv.second);
}
}
return res;
}
bool parse_csv(std::string_view content,
std::vector<std::vector<std::string>> &rows,
std::string *err_out) {
rows.clear();
size_t i = 0;
const size_t n = content.size();
while (i < n) {
// Skip empty lines (matching Go encoding/csv behavior)
if (content[i] == '\n') {
++i;
continue;
}
if (content[i] == '\r' && i + 1 < n && content[i + 1] == '\n') {
i += 2;
continue;
}
std::vector<std::string> row;
bool line_done = false;
while (!line_done) {
std::string field;
if (i < n && content[i] == '"') {
// Quoted field
++i;
bool closed = false;
while (i < n) {
if (content[i] == '"') {
if (i + 1 < n && content[i + 1] == '"') {
field.push_back('"');
i += 2;
} else {
++i;
closed = true;
break;
}
} else {
if (content[i] == '\r' && i + 1 < n && content[i + 1] == '\n') {
field.push_back('\n');
i += 2;
} else {
field.push_back(content[i]);
++i;
}
}
}
if (!closed) {
if (err_out) {
*err_out = "parse error: bare or unclosed \" in quoted-field";
}
return false;
}
if (i < n) {
if (content[i] == ',') {
row.push_back(std::move(field));
++i;
if (i == n || content[i] == '\n' ||
(content[i] == '\r' && i + 1 < n && content[i + 1] == '\n')) {
row.emplace_back("");
if (i < n && content[i] == '\r') {
i += 2;
} else if (i < n && content[i] == '\n') {
++i;
}
line_done = true;
}
} else if (content[i] == '\n') {
row.push_back(std::move(field));
++i;
line_done = true;
} else if (content[i] == '\r' && i + 1 < n &&
content[i + 1] == '\n') {
row.push_back(std::move(field));
i += 2;
line_done = true;
} else {
if (err_out) {
*err_out = "parse error: extraneous or missing \" in quoted-field";
}
return false;
}
} else {
row.push_back(std::move(field));
line_done = true;
}
} else {
// Unquoted field
while (i < n && content[i] != ',' && content[i] != '\n' &&
!(content[i] == '\r' && i + 1 < n && content[i + 1] == '\n')) {
if (content[i] == '"') {
if (err_out) {
*err_out = "parse error: bare \" in non-quoted-field";
}
return false;
}
field.push_back(content[i]);
++i;
}
row.push_back(std::move(field));
if (i < n && content[i] == ',') {
++i;
if (i == n || content[i] == '\n' ||
(content[i] == '\r' && i + 1 < n && content[i + 1] == '\n')) {
row.emplace_back("");
if (i < n && content[i] == '\r') {
i += 2;
} else if (i < n && content[i] == '\n') {
++i;
}
line_done = true;
}
} else {
if (i < n && content[i] == '\r' && i + 1 < n &&
content[i + 1] == '\n') {
i += 2;
} else if (i < n && content[i] == '\n') {
++i;
}
line_done = true;
}
}
}
rows.push_back(std::move(row));
}
return true;
}
std::vector<std::string> parse_auth_tokens(std::string_view content) {
std::vector<std::string> tokens;
size_t pos = 0;
while (pos <= content.size()) {
size_t nl = content.find('\n', pos);
std::string_view line = (nl == std::string_view::npos)
? content.substr(pos)
: content.substr(pos, nl - pos);
std::string token = str_trim(line);
if (!token.empty()) {
tokens.push_back(std::move(token));
}
if (nl == std::string_view::npos) {
break;
}
pos = nl + 1;
}
return tokens;
}
ConfigManager::ConfigManager(std::string file_path)
: file_path_(std::move(file_path)) {
std::string err;
if (!load(&err)) {
log_msg(
"Initial configuration load failed or file not found (will retry on change): %s",
err.c_str());
}
}
ConfigManager::ConfigManager(std::vector<ModelConfig> configs,
std::vector<std::string> unique_models)
: configs_(std::move(configs)), unique_models_(std::move(unique_models)) {}
bool ConfigManager::load(std::string *err_out) {
std::error_code ec;
auto mod_time = std::filesystem::last_write_time(file_path_, ec);
if (ec) {
if (err_out) {
*err_out = ec.message();
}
return false;
}
auto fsize = std::filesystem::file_size(file_path_, ec);
if (ec) {
if (err_out) {
*err_out = ec.message();
}
return false;
}
std::ifstream ifs(file_path_, std::ios::binary);
if (!ifs.is_open()) {
if (err_out) {
*err_out = "failed to open " + file_path_;
}
return false;
}
std::ostringstream oss;
oss << ifs.rdbuf();
std::string content = oss.str();
std::vector<std::vector<std::string>> rows;
if (!parse_csv(content, rows, err_out)) {
return false;
}
std::vector<ModelConfig> configs;
if (rows.empty()) {
update(std::move(configs), mod_time, fsize, true);
return true;
}
// Detect header row
bool has_header = false;
const auto &first_row = rows[0];
for (const auto &col : first_row) {
std::string col_lower = str_to_lower(str_trim(col));
if (col_lower == "model" || col_lower == "key" || col_lower == "endpoint") {
has_header = true;
break;
}
}
size_t start_idx = 0;
int model_idx = 0;
int key_idx = 1;
int endpoint_idx = 2;
int extra_idx = -1;
if (has_header) {
start_idx = 1;
model_idx = -1;
key_idx = -1;
endpoint_idx = -1;
extra_idx = -1;
for (size_t i = 0; i < first_row.size(); ++i) {
std::string col_lower = str_to_lower(str_trim(first_row[i]));
if (col_lower == "model") {
model_idx = static_cast<int>(i);
} else if (col_lower == "key") {
key_idx = static_cast<int>(i);
} else if (col_lower == "endpoint") {
endpoint_idx = static_cast<int>(i);
} else if (col_lower == "extra") {
extra_idx = static_cast<int>(i);
}
}
if (model_idx == -1) {
model_idx = 0;
}
if (key_idx == -1) {
key_idx = 1;
}
if (endpoint_idx == -1) {
endpoint_idx = 2;
}
} else {
start_idx = 0;
}
for (size_t i = start_idx; i < rows.size(); ++i) {
const auto &row = rows[i];
int max_idx = std::max({model_idx, key_idx, endpoint_idx});
if (static_cast<int>(row.size()) <= max_idx) {
continue;
}
std::string m = str_trim(row[static_cast<size_t>(model_idx)]);
std::string k = str_trim(row[static_cast<size_t>(key_idx)]);
std::string ep = str_trim(row[static_cast<size_t>(endpoint_idx)]);
if (m.empty() || k.empty() || ep.empty()) {
continue;
}
std::string extra;
if (has_header) {
if (extra_idx != -1 && static_cast<int>(row.size()) > extra_idx) {
extra = str_trim(row[static_cast<size_t>(extra_idx)]);
}
} else {
if (row.size() > 3) {
extra = str_trim(row[3]);
}
}
configs.push_back(ModelConfig{
std::move(m),
std::move(k),
std::move(ep),
std::move(extra),
});
}
size_t count = configs.size();
update(std::move(configs), mod_time, fsize, true);
log_msg("Loaded %zu model configurations from %s", count, file_path_.c_str());
return true;
}
void ConfigManager::update(std::vector<ModelConfig> configs,
std::filesystem::file_time_type mod_time,
std::uintmax_t size,
bool has_stat) {
std::unique_lock<std::shared_mutex> lock(mu_);
configs_ = std::move(configs);
last_mod_time_ = mod_time;
last_size_ = size;
has_stat_ = has_stat;
std::vector<std::string> unique_models;
std::unordered_set<std::string> seen;
for (const auto &cfg : configs_) {
if (seen.insert(cfg.model).second) {
unique_models.push_back(cfg.model);
}
}
unique_models_ = std::move(unique_models);
}
std::vector<ModelConfig> ConfigManager::get_configs() const {
std::shared_lock<std::shared_mutex> lock(mu_);
return configs_;
}
std::vector<std::string> ConfigManager::get_unique_models() const {
std::shared_lock<std::shared_mutex> lock(mu_);
return unique_models_;
}
void ConfigManager::watch(std::atomic<bool> &stop_flag) {
while (!stop_flag.load()) {
for (int i = 0; i < 20 && !stop_flag.load(); ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
if (stop_flag.load()) {
return;
}
std::error_code ec;
auto mod_time = std::filesystem::last_write_time(file_path_, ec);
if (ec) {
continue;
}
auto fsize = std::filesystem::file_size(file_path_, ec);
if (ec) {
continue;
}
bool changed = false;
{
std::shared_lock<std::shared_mutex> lock(mu_);
if (!has_stat_ || mod_time > last_mod_time_ || fsize != last_size_) {
changed = true;
}
}
if (changed) {
log_msg("Detected changes in %s, reloading...", file_path_.c_str());
std::string err;
if (!load(&err)) {
log_msg("Error reloading configuration: %s", err.c_str());
} else {
log_msg("Configuration reloaded successfully");
}
}
}
}
void start_csv_updater(std::atomic<bool> &stop_flag,
const std::string &command,
std::chrono::milliseconds interval) {
auto run_updater = [&]() {
log_msg("Running CSV updater command: %s", command.c_str());
int exit_status = 0;
std::string output = run_shell_command(command, exit_status);
if (exit_status != 0) {
std::string trimmed = str_trim(output);
log_msg("CSV updater command failed: exit status %d, output: \"%s\"",
exit_status, trimmed.c_str());
} else {
log_msg("CSV updater command completed successfully");
}
};
// Run once immediately at startup
run_updater();
while (!stop_flag.load()) {
auto start = std::chrono::steady_clock::now();
while (!stop_flag.load()) {
auto elapsed = std::chrono::steady_clock::now() - start;
if (elapsed >= interval) {
break;
}
auto remaining = std::chrono::duration_cast<std::chrono::milliseconds>(
interval - elapsed);
auto sleep_dur = std::min(remaining, std::chrono::milliseconds(50));
if (sleep_dur.count() > 0) {
std::this_thread::sleep_for(sleep_dur);
}
}
if (stop_flag.load()) {
return;
}
run_updater();
}
}