577 lines
15 KiB
C++
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();
|
|
}
|
|
}
|