/* 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 #include #include #include #include #include #include #include #include #include #include #include #include #include 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(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(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 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(sv[start]))) { ++start; } size_t end = sv.size(); while (end > start && std::isspace(static_cast(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(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(a[i])) != std::tolower(static_cast(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 HeaderList::values(std::string_view key) const { std::vector 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> &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 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 parse_auth_tokens(std::string_view content) { std::vector 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 configs, std::vector 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> rows; if (!parse_csv(content, rows, err_out)) { return false; } std::vector 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(i); } else if (col_lower == "key") { key_idx = static_cast(i); } else if (col_lower == "endpoint") { endpoint_idx = static_cast(i); } else if (col_lower == "extra") { extra_idx = static_cast(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(row.size()) <= max_idx) { continue; } std::string m = str_trim(row[static_cast(model_idx)]); std::string k = str_trim(row[static_cast(key_idx)]); std::string ep = str_trim(row[static_cast(endpoint_idx)]); if (m.empty() || k.empty() || ep.empty()) { continue; } std::string extra; if (has_header) { if (extra_idx != -1 && static_cast(row.size()) > extra_idx) { extra = str_trim(row[static_cast(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 configs, std::filesystem::file_time_type mod_time, std::uintmax_t size, bool has_stat) { std::unique_lock lock(mu_); configs_ = std::move(configs); last_mod_time_ = mod_time; last_size_ = size; has_stat_ = has_stat; std::vector unique_models; std::unordered_set 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 ConfigManager::get_configs() const { std::shared_lock lock(mu_); return configs_; } std::vector ConfigManager::get_unique_models() const { std::shared_lock lock(mu_); return unique_models_; } void ConfigManager::watch(std::atomic &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 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 &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( 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(); } }