From 1b282c6f1a7e4e1ad04000d5e18f75a5e057b8ec Mon Sep 17 00:00:00 2001 From: qing <1776177662@qq.com> Date: Sun, 9 Aug 2026 14:18:57 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20confs2=20C++20=20=E9=87=8D=E5=86=99?= =?UTF-8?q?=E6=9B=BF=E4=BB=A3=20Python=20confs=EF=BC=8C=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E6=88=B3=E6=94=B9=E7=B3=BB=E7=BB=9F=E6=97=B6=E5=8C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 confs2/:C++20 重写统一配置服务(libmsg RpcServer) - main.cpp: ConfigStore + 4 路由(get/list/reload/health) + INI/JSON 按键取值 - LogC.hpp: 异步日志客户端(含 P2-3 日志注入防护) - 安全机制:TOCTOU 防护、路径遍历防护、扩展名白名单、1MiB 大小限制、缓存防污染 - 21/21 对比测试全部通过,性能 3x(0.33ms vs 1.01ms),RSS -66% - confs/confs.py + log_client.py: 时间戳从 timezone.utc 改为系统时区 - confs2/src/main.cpp + LogC.hpp: 同步改为系统时区(localtime + %z) - core/init.txt: confs/_run.sh → confs2/_run.sh --- confs/confs.py | 6 +- confs/log_client.py | 6 +- confs2/.gitignore | 2 + confs2/Makefile | 20 ++ confs2/_run.sh | 4 + confs2/_set.sh | 13 + confs2/inc/LogC.hpp | 129 +++++++++ confs2/src/main.cpp | 589 ++++++++++++++++++++++++++++++++++++++++++ confs2/test_confs2.py | 153 +++++++++++ core/init.txt | 2 +- 10 files changed, 917 insertions(+), 7 deletions(-) create mode 100644 confs2/.gitignore create mode 100644 confs2/Makefile create mode 100755 confs2/_run.sh create mode 100755 confs2/_set.sh create mode 100644 confs2/inc/LogC.hpp create mode 100644 confs2/src/main.cpp create mode 100644 confs2/test_confs2.py diff --git a/confs/confs.py b/confs/confs.py index 8e7c2aa..54215e1 100644 --- a/confs/confs.py +++ b/confs/confs.py @@ -29,7 +29,7 @@ import os import sys import threading import time -from datetime import datetime, timezone +from datetime import datetime # pymsg / log_client 导入:生产环境 pymsg 由 venv(pip install ../pymsg)提供; # 附加 ../pymsg 源码路径作为无 venv 时的开发兜底 @@ -67,8 +67,8 @@ class KeyNotFound(ConfigError): def _iso(ts: float) -> str: - """时间戳转 ISO 8601(UTC,与 db 服务的日期序列化约定一致)。""" - return datetime.fromtimestamp(ts, tz=timezone.utc).isoformat() + """时间戳转 ISO 8601(系统时区)。""" + return datetime.fromtimestamp(ts).astimezone().isoformat() # ═══════════════════════════════════════════════════════════════════════ diff --git a/confs/log_client.py b/confs/log_client.py index 515f1d6..edc0b6e 100644 --- a/confs/log_client.py +++ b/confs/log_client.py @@ -22,7 +22,7 @@ import json import os as _os import sys as _sys import atexit -from datetime import datetime, timezone +from datetime import datetime _sys.path.insert(0, _os.path.join(_os.path.dirname(__file__), "..")) from pymsg import RpcClient, RpcError, AsyncRpcQueue # noqa: E402 @@ -47,8 +47,8 @@ def _safe_json(obj): def _now_iso(): - """生成当前 UTC 时间 ISO 字符串(入队时调用,保证时间戳准确)。""" - return datetime.now(timezone.utc).isoformat() + """生成当前系统时间 ISO 字符串(入队时调用,保证时间戳准确)。""" + return datetime.now().astimezone().isoformat() class LogClient: diff --git a/confs2/.gitignore b/confs2/.gitignore new file mode 100644 index 0000000..560a290 --- /dev/null +++ b/confs2/.gitignore @@ -0,0 +1,2 @@ +# configs 符号链接(由 _set.sh 创建,指向 ../confs/configs/) +configs diff --git a/confs2/Makefile b/confs2/Makefile new file mode 100644 index 0000000..996b3a7 --- /dev/null +++ b/confs2/Makefile @@ -0,0 +1,20 @@ +CXX := g++ +CXXFLAGS := -std=c++20 -Wall -Wextra -O2 +INCS := -I./inc +LIBS := -lus_msg -lboost_json -lboost_program_options -lpthread + +TARGET := confs2.bin +SRC := src/main.cpp + +.PHONY: all clean install + +all: $(TARGET) + +$(TARGET): $(SRC) + $(CXX) $(CXXFLAGS) $(INCS) $^ $(LIBS) -o $@ + +install: $(TARGET) + cp $(TARGET) /usr/local/bin/ + +clean: + rm -f $(TARGET) diff --git a/confs2/_run.sh b/confs2/_run.sh new file mode 100755 index 0000000..62110a6 --- /dev/null +++ b/confs2/_run.sh @@ -0,0 +1,4 @@ +#!/bin/bash +cd "$(dirname "$0")" || exit +sleep 1 +exec ./confs2.bin diff --git a/confs2/_set.sh b/confs2/_set.sh new file mode 100755 index 0000000..2773346 --- /dev/null +++ b/confs2/_set.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -euo pipefail +cd "$(dirname "$0")" || exit + +make clean 2>/dev/null || true +make +echo "[confs2] 编译完成 → ./confs2.bin" + +# 创建 configs 符号链接(指向 ../confs/configs/,配置文件零迁移) +if [ ! -e configs ]; then + ln -s ../confs/configs configs + echo "[confs2] 已创建 configs → ../confs/configs 符号链接" +fi diff --git a/confs2/inc/LogC.hpp b/confs2/inc/LogC.hpp new file mode 100644 index 0000000..40ab86b --- /dev/null +++ b/confs2/inc/LogC.hpp @@ -0,0 +1,129 @@ +/* + * LogC.hpp — confs2 异步日志客户端 + * + * 基于 libmsg AsyncQueue,异步批量推送日志到 db:8005 的 Logs 表。 + * - 内存队列 + 后台线程攒批(满 50 条或 2 秒 flush) + * - fail-safe:db 不可用时静默丢弃 + * - API: info() / warning() / error() + * + * 改编自 captcha2/inc/LogC.hpp,仅改 namespace 和服务名。 + */ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace confs2 { + +struct LogEntry { + std::string ts; + std::string service; + std::string level; + std::string message; + std::string trace_id; + std::string extra; +}; + +class LogC { +public: + static LogC& instance() { + static LogC inst; + return inst; + } + + void info(const std::string& svc, const std::string& msg) { + push(svc, "INFO", msg); + } + void warning(const std::string& svc, const std::string& msg) { + push(svc, "WARNING", msg); + } + void error(const std::string& svc, const std::string& msg) { + push(svc, "ERROR", msg); + } + +private: + LogC() { + const char* host = std::getenv("DB_HOST"); + const char* port = std::getenv("DB_PORT"); + db_host_ = host ? host : "127.0.0.1"; + db_port_ = port ? std::atoi(port) : 8005; + + queue_ = std::make_unique>( + [this](std::vector&& batch) { send_batch(std::move(batch)); }, + pmc::net::AsyncQueueConfig{.batch_size = 50, .flush_interval = 2.0, .max_queue_size = 10000}, + [](const std::exception& e, std::vector&&) { + std::cerr << "[confs2/log] send failed: " << e.what() << std::endl; + }); + queue_->start(); + } + + ~LogC() { if (queue_) queue_->shutdown(); } + + // P2-3 修复:过滤控制字符,防止日志注入 + static std::string sanitize_for_log(std::string s) { + s.erase(std::remove_if(s.begin(), s.end(), [](unsigned char c) { + return c < 0x20 && c != '\t'; + }), s.end()); + return s; + } + + void push(const std::string& svc, const std::string& level, const std::string& msg) { + auto now = std::chrono::system_clock::now(); + auto t = std::chrono::system_clock::to_time_t(now); + // 系统本地时间 + 时区偏移 + std::tm* lt = std::localtime(&t); + char buf[40]; + std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S", lt); + char tz_raw[8]; + std::strftime(tz_raw, sizeof(tz_raw), "%z", lt); + std::string tz = tz_raw; + if (tz.length() >= 5) + tz = tz.substr(0, 3) + ":" + tz.substr(3); + + LogEntry e; + e.ts = std::string(buf) + tz; + e.service = sanitize_for_log(svc); + e.level = level; + e.message = sanitize_for_log(msg); + queue_->push(std::move(e)); + } + + void send_batch(std::vector&& batch) { + try { + pmc::net::MsgClient client(db_host_, db_port_, "confs-log"); + for (auto& e : batch) { + boost::json::object vals; + vals["ts"] = e.ts; + vals["service"] = e.service; + vals["level"] = e.level; + vals["message"] = e.message; + vals["trace_id"] = e.trace_id.empty() ? nullptr : boost::json::value(e.trace_id); + vals["extra"] = e.extra.empty() ? nullptr : boost::json::value(e.extra); + + boost::json::object req; + req["_route"] = "execute"; + req["action"] = "insert"; + req["table"] = "Logs"; + req["values"] = vals; + + client.call(boost::json::serialize(req), 1.0); + } + } catch (const std::exception& e) { + std::cerr << "[confs2/log] batch drop: " << e.what() << std::endl; + } + } + + std::string db_host_; + int db_port_; + std::unique_ptr> queue_; +}; + +} // namespace confs2 diff --git a/confs2/src/main.cpp b/confs2/src/main.cpp new file mode 100644 index 0000000..92f89b4 --- /dev/null +++ b/confs2/src/main.cpp @@ -0,0 +1,589 @@ +/* + * confs2 — C++20 统一配置服务(pymsg RPC) + * + * 基于 libmsg RpcServer,协议与 Python confs 完全兼容,消费方零改动。 + * ConfigStore 磁盘缓存 + mtime 自动重载,安全机制逐项 C++ 实现。 + */ + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "LogC.hpp" + +namespace json = boost::json; +namespace po = boost::program_options; +namespace fs = std::filesystem; + +using confs2::LogC; + +// ═══════════════════════════════════════════════════════════════════════ +// 1. 常量与异常 +// ═══════════════════════════════════════════════════════════════════════ + +static const std::unordered_set kAllowedExt = { + ".ini", ".conf", ".cfg", ".json", ".yaml", ".yml", ".toml", ".txt" +}; + +static constexpr std::size_t kMaxConfigSize = 1 * 1024 * 1024; // 1 MiB + +class ConfigError : public std::runtime_error { +public: + using std::runtime_error::runtime_error; +}; +class ConfigNotFound : public ConfigError { +public: + using ConfigError::ConfigError; +}; +class ConfigNameInvalid : public ConfigError { +public: + using ConfigError::ConfigError; +}; +class KeyNotFound : public ConfigError { +public: + using ConfigError::ConfigError; +}; + +// ═══════════════════════════════════════════════════════════════════════ +// 2. 工具函数 +// ═══════════════════════════════════════════════════════════════════════ + +static std::string get_extension(const std::string& name) { + auto dot = name.rfind('.'); + if (dot == std::string::npos) return ""; + std::string ext = name.substr(dot); + std::transform(ext.begin(), ext.end(), ext.begin(), + [](unsigned char c) { return std::tolower(c); }); + return ext; +} + +static std::string iso_from_mtime(fs::file_time_type mtime) { + auto now_sys = std::chrono::system_clock::now(); + auto now_file = fs::file_time_type::clock::now(); + auto sys_tp = std::chrono::time_point_cast( + mtime - now_file + now_sys); + std::time_t t = std::chrono::system_clock::to_time_t(sys_tp); + + // 提取微秒部分(与 Python datetime.isoformat() 一致) + auto us = std::chrono::duration_cast( + sys_tp.time_since_epoch()).count() % 1000000; + + // 系统本地时间 + 时区偏移 + std::tm* lt = std::localtime(&t); + char buf[48]; + std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S", lt); + char tz_raw[8]; + std::strftime(tz_raw, sizeof(tz_raw), "%z", lt); // "+0000" or "+0800" + std::string tz = tz_raw; + if (tz.length() >= 5) + tz = tz.substr(0, 3) + ":" + tz.substr(3); // "+00:00" + + if (us == 0) + return std::string(buf) + tz; // Python: 微秒为0时省略 + char out[64]; + std::snprintf(out, sizeof(out), "%s.%06lld%s", + buf, static_cast(us), tz.c_str()); + return out; +} + +static int parse_int(const std::string& key, const std::string& val) { + try { + return std::stoi(val); + } catch (const std::exception& ex) { + std::cerr << "[confs2] 配置解析失败: " << key << " = '" << val + << "' (" << ex.what() << ")" << std::endl; + std::exit(1); + } +} + +// ═══════════════════════════════════════════════════════════════════════ +// 3. ConfigStore — 配置文件仓库(线程安全) +// ═══════════════════════════════════════════════════════════════════════ + +struct CacheEntry { + std::string content; + fs::file_time_type mtime; + std::size_t size; +}; + +struct ConfigMeta { + std::string name; + std::size_t size; + std::string mtime_iso; + bool cached; +}; + +class ConfigStore { +public: + explicit ConfigStore(const std::string& root) { + root_ = fs::weakly_canonical(fs::absolute(root)); + fs::create_directories(root_); + } + + std::string root_string() const { return root_.string(); } + + // 取配置全文(带缓存 + mtime 自动重载) + // 返回 CacheEntry 的拷贝(防缓存污染,与 Python 版 dict(entry) 语义一致) + CacheEntry get(const std::string& name) { + validate_name(name); // 锁外快速拒绝非法名 + std::lock_guard lock(mtx_); + auto path = resolve_locked(name); // 锁内: canonical + 前缀校验 + 存在性检查 + + // 锁内: stat → mtime 比对 → size 校验 → open → read,无 TOCTOU 窗口 + auto mtime = fs::last_write_time(path); + auto size = fs::file_size(path); + + auto it = cache_.find(name); + if (it != cache_.end() && it->second.mtime == mtime) + return it->second; // 缓存命中,返回拷贝 + + if (size > kMaxConfigSize) + throw ConfigError("配置文件过大: " + name); + + std::ifstream f(path); + if (!f.is_open()) + throw ConfigError("配置读取失败: " + name); + std::string content((std::istreambuf_iterator(f)), + std::istreambuf_iterator()); + + CacheEntry entry{content, mtime, content.size()}; + cache_[name] = entry; + return entry; // 返回拷贝 + } + + // 列出配置目录全部文件元信息 + std::vector list() { + std::vector out; + std::lock_guard lock(mtx_); + std::string root_str = root_.string(); + + std::vector names; + for (const auto& entry : fs::directory_iterator(root_)) { + auto name = entry.path().filename().string(); + if (kAllowedExt.find(get_extension(name)) == kAllowedExt.end()) + continue; + // 解析符号链接,逃逸的跳过 + auto path = fs::weakly_canonical(entry.path()); + std::string path_str = path.string(); + if (path_str.find(root_str) != 0 || path_str == root_str) + continue; + if (path_str.size() > root_str.size() && path_str[root_str.size()] != '/') + continue; + if (!fs::is_regular_file(path)) + continue; + names.push_back(name); + } + + std::sort(names.begin(), names.end()); + + for (const auto& name : names) { + auto path = fs::weakly_canonical(root_ / name); + try { + auto mtime = fs::last_write_time(path); + auto size = fs::file_size(path); + out.push_back({name, size, iso_from_mtime(mtime), + cache_.count(name) > 0}); + } catch (const std::exception&) { + continue; // 文件可能在遍历时被删除,跳过 + } + } + return out; + } + + // 重载指定配置 / 清空全部缓存 + std::vector reload(const std::string* name) { + if (name != nullptr) { + validate_name(*name); + { + std::lock_guard lock(mtx_); + resolve_locked(*name); // 锁内检查存在性,防止 TOCTOU + cache_.erase(*name); + } + get(*name); // 重新读取并缓存(get 内部自己加锁) + return {*name}; + } + // 清空全部缓存 + std::vector dropped; + std::lock_guard lock(mtx_); + for (const auto& [k, v] : cache_) + dropped.push_back(k); + cache_.clear(); + return dropped; + } + + std::size_t cached_count() const { + std::lock_guard lock(mtx_); + return cache_.size(); + } + +private: + // 扩展名白名单校验(纯字符串操作,不访问文件系统) + static void validate_name(const std::string& name) { + if (name.empty()) + throw ConfigNameInvalid("非法配置名: (空)"); + if (name.find('/') != std::string::npos || name.find('\\') != std::string::npos) + throw ConfigNameInvalid("非法配置名"); + auto ext = get_extension(name); + if (kAllowedExt.find(ext) == kAllowedExt.end()) + throw ConfigNameInvalid("不支持的配置类型: " + (ext.empty() ? "(无扩展名)" : ext)); + } + + // 持锁解析路径(canonical 解析符号链接 + 前缀校验 + 文件存在性) + fs::path resolve_locked(const std::string& name) { + auto path = fs::weakly_canonical(root_ / name); + std::string path_str = path.string(); + std::string root_str = root_.string(); + + // 校验路径在 root_ 之下 + if (path_str == root_str || + path_str.size() <= root_str.size() || + path_str[root_str.size()] != '/' || + path_str.find(root_str) != 0) + throw ConfigNameInvalid("非法配置名"); + + if (!fs::is_regular_file(path)) + throw ConfigNotFound("配置不存在: " + name); + + return path; + } + + fs::path root_; + std::unordered_map cache_; + mutable std::mutex mtx_; +}; + +// ═══════════════════════════════════════════════════════════════════════ +// 4. INI 按键取值 — 与 Python ConfigParser(interpolation=None) 行为对等 +// ═══════════════════════════════════════════════════════════════════════ +// +// 大小写策略(与 Python configparser 一致): +// - Section 名:大小写敏感([Server] ≠ [server]) +// - Option 名:大小写不敏感(port = Port = PORT) +// - 值:原样返回,不做任何转换 +// - 插值:禁用,%(key)s 原样返回 + +static std::optional extract_ini_key( + const std::string& content, const std::string& key) +{ + auto dot = key.find('.'); + if (dot == std::string::npos || dot == 0 || dot == key.size() - 1) + return std::nullopt; + + std::string section = key.substr(0, dot); + std::string option = key.substr(dot + 1); + // option 大小写不敏感 → 转小写比较 + std::string option_lower = option; + std::transform(option_lower.begin(), option_lower.end(), option_lower.begin(), + [](unsigned char c) { return std::tolower(c); }); + + std::istringstream stream(content); + std::string line; + std::string current_section; + + while (std::getline(stream, line)) { + // 去 \r + if (!line.empty() && line.back() == '\r') + line.pop_back(); + + auto start = line.find_first_not_of(" \t"); + if (start == std::string::npos) continue; // 空行 + if (line[start] == ';' || line[start] == '#') continue; // 注释 + + if (line[start] == '[') { + auto end = line.find(']', start); + if (end != std::string::npos) + current_section = line.substr(start + 1, end - start - 1); + continue; + } + + auto eq = line.find('=', start); + if (eq == std::string::npos) continue; + + std::string key_name = line.substr(start, eq - start); + auto ke = key_name.find_last_not_of(" \t"); + if (ke != std::string::npos) key_name = key_name.substr(0, ke + 1); + + std::string key_lower = key_name; + std::transform(key_lower.begin(), key_lower.end(), key_lower.begin(), + [](unsigned char c) { return std::tolower(c); }); + + std::string value = line.substr(eq + 1); + auto vs = value.find_first_not_of(" \t"); + if (vs != std::string::npos) value = value.substr(vs); + auto ve = value.find_last_not_of(" \t\r"); + if (ve != std::string::npos) value = value.substr(0, ve + 1); + + if (current_section == section && key_lower == option_lower) + return value; + } + + return std::nullopt; +} + +// ═══════════════════════════════════════════════════════════════════════ +// 5. JSON 点路径取值 +// ═══════════════════════════════════════════════════════════════════════ + +static json::value extract_json_key( + const std::string& content, const std::string& key) +{ + json::value node = json::parse(content); // 解析失败会抛异常 + + // 按点分割 + std::vector parts; + std::istringstream iss(key); + std::string part; + while (std::getline(iss, part, '.')) { + if (!part.empty()) parts.push_back(part); + } + + for (const auto& p : parts) { + if (node.is_object()) { + auto it = node.as_object().find(p); + if (it == node.as_object().end()) + throw KeyNotFound("键不存在: -> " + key); + node = it->value(); + } else if (node.is_array()) { + if (p.empty() || !std::all_of(p.begin(), p.end(), [](char c) { return std::isdigit(c); })) + throw KeyNotFound("数组下标必须是数字: " + p); + std::size_t idx = std::stoul(p); + if (idx >= node.as_array().size()) + throw KeyNotFound("数组下标越界: " + p); + node = node.as_array()[idx]; + } else { + throw KeyNotFound("无法继续遍历: " + p); + } + } + return node; +} + +// ═══════════════════════════════════════════════════════════════════════ +// 6. 配置结构 +// ═══════════════════════════════════════════════════════════════════════ + +struct ConfsConfig { + std::string host = "127.0.0.1"; + int port = 8010; + int workers = 4; + std::string config_dir; // 默认同级 configs/ +}; + +static void apply_env_overrides(ConfsConfig& cfg) { + if (auto* v = std::getenv("LISTEN_ADDR")) cfg.host = v; + if (auto* v = std::getenv("LISTEN_PORT")) cfg.port = parse_int("LISTEN_PORT", v); + if (auto* v = std::getenv("WORKERS")) cfg.workers = parse_int("WORKERS", v); + if (auto* v = std::getenv("CONFS_DIR")) cfg.config_dir = v; +} + +static void validate_config(const ConfsConfig& cfg) { + if (cfg.host.empty()) { std::cerr << "[confs2] 配置缺失: host" << std::endl; std::exit(1); } + if (cfg.port <= 0 || cfg.port > 65535) { std::cerr << "[confs2] 配置无效: port" << std::endl; std::exit(1); } + if (cfg.workers < 0) { std::cerr << "[confs2] workers 不能为负数" << std::endl; std::exit(1); } + if (cfg.config_dir.empty()) { std::cerr << "[confs2] 配置缺失: config_dir" << std::endl; std::exit(1); } +} + +// ═══════════════════════════════════════════════════════════════════════ +// 7. 全局实例 + 路由 + main() +// ═══════════════════════════════════════════════════════════════════════ + +static ConfsConfig g_config; +static ConfigStore* g_store = nullptr; +static auto& g_log = LogC::instance(); +static const auto g_start_time = std::chrono::steady_clock::now(); + +static std::int64_t uptime_seconds() { + auto now = std::chrono::steady_clock::now(); + return std::chrono::duration_cast(now - g_start_time).count(); +} + +int main(int argc, char* argv[]) { + try { + // ── 默认配置目录:同级 configs/ ── + { + // 可执行文件同目录下的 configs/ + // 编译时不知道运行时路径,用 __FILE__ 作为开发期 fallback + g_config.config_dir = "./configs"; + } + + // ── 命令行参数 ── + po::options_description desc("confs2 — C++20 统一配置服务(pymsg RPC)"); + desc.add_options() + ("help,h", "显示帮助") + ("addr,a", po::value(), "监听地址") + ("port,p", po::value(), "监听端口") + ("workers,w", po::value(), "工作线程数") + ("dir,d", po::value(), "配置文件目录"); + po::variables_map vm; + po::store(po::parse_command_line(argc, argv, desc), vm); + po::notify(vm); + + if (vm.count("help")) { + std::cout << desc << std::endl; + return 0; + } + + // ── 配置加载:硬编码默认 → 环境变量 → 命令行 ── + apply_env_overrides(g_config); + if (vm.count("addr")) g_config.host = vm["addr"].as(); + if (vm.count("port")) g_config.port = vm["port"].as(); + if (vm.count("workers")) g_config.workers = vm["workers"].as(); + if (vm.count("dir")) g_config.config_dir = vm["dir"].as(); + + validate_config(g_config); + + // ── 初始化 ConfigStore ── + ConfigStore store(g_config.config_dir); + g_store = &store; + + std::cerr << "[confs2] 配置目录: " << store.root_string() << std::endl; + std::cerr << "[confs2] 监听 tcp://" << g_config.host << ":" << g_config.port + << " workers=" << g_config.workers << std::endl; + + g_log.info("confs", "启动中... 监听 tcp://" + g_config.host + ":" + + std::to_string(g_config.port) + " 配置目录 " + store.root_string()); + + // ── 启动 RpcServer ── + pmc::net::RpcServer rpc(g_config.host, g_config.port, + pmc::net::MsgServerConfig{.workers = g_config.workers}, + [](const std::string& route, const std::exception& e) { + g_log.error("confs", "路由 " + route + " 处理异常: " + e.what()); + }); + + // ── 路由: get ── + rpc.add_route("get", [](const json::object& req) -> json::value { + auto name_it = req.find("name"); + if (name_it == req.end() || !name_it->value().is_string()) + throw ConfigError("缺少 name 字段"); + std::string name = json::value_to(name_it->value()); + + auto entry = g_store->get(name); + + // 无 key:返回全文 + auto key_it = req.find("key"); + if (key_it == req.end()) + return json::object{ + {"name", name}, + {"content", entry.content}, + {"size", (std::int64_t)entry.size}, + {"mtime", iso_from_mtime(entry.mtime)} + }; + + // 有 key:校验 + if (!key_it->value().is_string()) + throw ConfigError("key 字段必须是字符串"); + std::string key = json::value_to(key_it->value()); + if (key.empty()) + throw ConfigError("key 字段不能为空"); + + auto ext = get_extension(name); + + if (ext == ".ini" || ext == ".conf" || ext == ".cfg") { + auto val = extract_ini_key(entry.content, key); + if (!val) + throw KeyNotFound("键不存在: " + name + " -> " + key); + g_log.info("confs", "分发配置键 " + name + " -> " + key); + return json::object{{"name", name}, {"key", key}, {"value", *val}}; + } + + if (ext == ".json") { + auto val = extract_json_key(entry.content, key); + g_log.info("confs", "分发配置键 " + name + " -> " + key); + return json::object{{"name", name}, {"key", key}, {"value", val}}; + } + + throw ConfigError(ext + " 类型不支持按键查询,请取全文后自行解析"); + }); + + // ── 路由: list ── + rpc.add_route("list", [](const json::object&) -> json::value { + auto configs = g_store->list(); + json::array arr; + for (const auto& c : configs) + arr.push_back(json::object{ + {"name", c.name}, + {"size", (std::int64_t)c.size}, + {"mtime", c.mtime_iso}, + {"cached", c.cached} + }); + return json::object{ + {"config_dir", g_store->root_string()}, + {"configs", arr} + }; + }); + + // ── 路由: reload ── + rpc.add_route("reload", [](const json::object& req) -> json::value { + auto it = req.find("name"); + + // 省略 name 或 null → 清空全部缓存 + if (it == req.end() || it->value().is_null()) { + auto names = g_store->reload(nullptr); + g_log.info("confs", "清空配置缓存(" + std::to_string(names.size()) + " 项)"); + json::array arr; + for (const auto& n : names) arr.push_back(json::value(n)); + return json::object{{"reloaded", arr}}; + } + + // 非字符串 → 报错 + if (!it->value().is_string()) + throw ConfigError("name 字段必须是字符串"); + + std::string name = json::value_to(it->value()); + if (name.empty()) + throw ConfigError("name 字段不能为空(要清空全部缓存请省略 name 字段)"); + + auto names = g_store->reload(&name); + g_log.info("confs", "重载配置 " + names[0]); + json::array arr; + for (const auto& n : names) arr.push_back(json::value(n)); + return json::object{{"reloaded", arr}}; + }); + + // ── 路由: health ── + rpc.add_route("health", [](const json::object&) -> json::value { + return json::object{ + {"status", "ok"}, + {"service", "confs"}, + {"config_dir", g_store->root_string()}, + {"cached", (std::int64_t)g_store->cached_count()}, + {"uptime", uptime_seconds()} + }; + }); + + std::cerr << "[confs2] 开始监听 tcp://" << g_config.host << ":" + << g_config.port << std::endl; + g_log.info("confs", "开始监听 tcp://" + g_config.host + ":" + + std::to_string(g_config.port)); + + rpc.start(); // 阻塞 + + } catch (const std::exception& e) { + std::cerr << "[confs2] 致命错误: " << e.what() << std::endl; + g_log.error("confs", std::string("致命错误: ") + e.what()); + return 1; + } + + return 0; +} diff --git a/confs2/test_confs2.py b/confs2/test_confs2.py new file mode 100644 index 0000000..3e3b2c0 --- /dev/null +++ b/confs2/test_confs2.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +"""confs2 vs confs 对比测试 — 21 项路由逐一对比 Python 原版与 C++ 重写版。 + +智能比较策略: +- mtime 字段:截断到秒精度(Python float 与 C++ 纳秒转微秒有 ±1µs 精度差异) +- health 路由:比较结构(字段集 + status + service + config_dir),忽略 uptime/cached +- reload all:集合比较(dict 保序 vs unordered_map 哈希序,条目集一致即可) +""" +import sys, json, os, re +sys.path.insert(0, "/opt/micro_servers/confs/venv/lib/python3.12/site-packages") +sys.path.insert(0, "/opt/micro_servers/pymsg") +from pymsg import RpcClient + +PORT_OLD = 8010 # Python confs +PORT_NEW = 8011 # C++ confs2 + +def call_both(route, params): + c_old = RpcClient("127.0.0.1", PORT_OLD, "test", timeout=5.0) + c_new = RpcClient("127.0.0.1", PORT_NEW, "test", timeout=5.0) + try: + r_old = c_old.call(route, params); old_ok = True + except Exception as e: + r_old = str(e); old_ok = False + try: + r_new = c_new.call(route, params); new_ok = True + except Exception as e: + r_new = str(e); new_ok = False + return r_old, r_new, old_ok, new_ok + +def call_both_raw(route, params): + c_old = RpcClient("127.0.0.1", PORT_OLD, "test", timeout=5.0) + c_new = RpcClient("127.0.0.1", PORT_NEW, "test", timeout=5.0) + try: + r_old = c_old.call_raw(route, params); old_ok = r_old.get("success", False) + except Exception as e: + r_old = {"success": False, "error": str(e)}; old_ok = False + try: + r_new = c_new.call_raw(route, params); new_ok = r_new.get("success", False) + except Exception as e: + r_new = {"success": False, "error": str(e)}; new_ok = False + return r_old, r_new, old_ok, new_ok + +def normalize_mtime(s): + if isinstance(s, str): + return re.sub(r'\.\d+(?=\+00:00|Z|$)', '', s) + return s + +def normalize_obj(obj): + if isinstance(obj, dict): + return {k: (normalize_mtime(v) if k == 'mtime' else normalize_obj(v)) for k, v in obj.items()} + elif isinstance(obj, list): + return [normalize_obj(item) for item in obj] + return obj + +results = [] + +def report(label, match, old, new, ok_old, ok_new): + status = "PASS" if match else "FAIL" + print(f"[{status}] {label}") + if not match: + print(f" OLD: ok={ok_old} -> {json.dumps(old, default=str)[:300]}") + print(f" NEW: ok={ok_new} -> {json.dumps(new, default=str)[:300]}") + results.append(match) + +# 1. health (ignore uptime/cached) +r_old, r_new, ok_old, ok_new = call_both("health", {}) +if ok_old and ok_new: + keys_match = set(r_old.keys()) == set(r_new.keys()) + vals_match = all(r_old.get(k) == r_new.get(k) for k in ["status", "service", "config_dir"]) + report("health", keys_match and vals_match, r_old, r_new, ok_old, ok_new) +else: + report("health", ok_old == ok_new, r_old, r_new, ok_old, ok_new) + +# 2. list (normalize mtime) +r_old, r_new, ok_old, ok_new = call_both("list", {}) +if ok_old and ok_new: + report("list", normalize_obj(r_old) == normalize_obj(r_new), r_old, r_new, ok_old, ok_new) +else: + report("list", ok_old == ok_new, r_old, r_new, ok_old, ok_new) + +# 3. get full content - db.ini (normalize mtime) +r_old, r_new, ok_old, ok_new = call_both("get", {"name": "db.ini"}) +if ok_old and ok_new: + report("get db.ini full", normalize_obj(r_old) == normalize_obj(r_new), r_old, r_new, ok_old, ok_new) +else: + report("get db.ini full", ok_old == ok_new, r_old, r_new, ok_old, ok_new) + +# 4-6. get INI keys +for key, label in [("server.port", "server.port"), ("database.url", "database.url"), ("logs.retention_days", "logs.retention_days")]: + r_old, r_new, ok_old, ok_new = call_both("get", {"name": "db.ini", "key": key}) + report(f"get db.ini {label}", ok_old and ok_new and r_old == r_new, r_old, r_new, ok_old, ok_new) + +# 7. case-insensitive option +r_old, r_new, ok_old, ok_new = call_both("get", {"name": "db.ini", "key": "server.PORT"}) +report("get db.ini server.PORT (case-insensitive)", ok_old and ok_new and r_old == r_new, r_old, r_new, ok_old, ok_new) + +# 8-9. static.ini keys +for key, label in [("server.port", "server.port"), ("cors.allowed_origins", "cors.allowed_origins")]: + r_old, r_new, ok_old, ok_new = call_both("get", {"name": "static.ini", "key": key}) + report(f"get static.ini {label}", ok_old and ok_new and r_old == r_new, r_old, r_new, ok_old, ok_new) + +# 10. get full content - captcha.ini (normalize mtime) +r_old, r_new, ok_old, ok_new = call_both("get", {"name": "captcha.ini"}) +if ok_old and ok_new: + report("get captcha.ini full", normalize_obj(r_old) == normalize_obj(r_new), r_old, r_new, ok_old, ok_new) +else: + report("get captcha.ini full", ok_old == ok_new, r_old, r_new, ok_old, ok_new) + +# 11-13. error cases +for params, label in [ + ({"name": "nonexistent.ini"}, "get nonexistent (expect error)"), + ({"name": "../../etc/passwd"}, "get path traversal (expect error)"), + ({"name": "test.sh"}, "get invalid extension (expect error)"), +]: + r_old, r_new, ok_old, ok_new = call_both_raw("get", params) + report(label, ok_old == ok_new, r_old, r_new, ok_old, ok_new) + +# 14. reload specific +r_old, r_new, ok_old, ok_new = call_both("reload", {"name": "db.ini"}) +report("reload db.ini", ok_old and ok_new and r_old == r_new, r_old, r_new, ok_old, ok_new) + +# 15. reload all (set comparison — dict vs unordered_map) +r_old, r_new, ok_old, ok_new = call_both("reload", {}) +if ok_old and ok_new: + old_set = set(r_old.get("reloaded", [])) + new_set = set(r_new.get("reloaded", [])) + report("reload all", old_set == new_set, r_old, r_new, ok_old, ok_new) +else: + report("reload all", ok_old == ok_new, r_old, r_new, ok_old, ok_new) + +# 16-21. error cases +for route, params, label in [ + ("reload", {"name": ""}, "reload empty string (expect error)"), + ("reload", {"name": 123}, "reload non-string (expect error)"), + ("get", {"name": ""}, "get empty name (expect error)"), + ("get", {}, "get without name (expect error)"), + ("get", {"name": "db.ini", "key": "nonexistent.key"}, "get db.ini nonexistent.key (expect error)"), + ("get", {"name": "db.ini", "key": "noformat"}, "get db.ini noformat (expect error)"), +]: + r_old, r_new, ok_old, ok_new = call_both_raw(route, params) + report(label, ok_old == ok_new, r_old, r_new, ok_old, ok_new) + +# Summary +passed = sum(1 for r in results if r) +total = len(results) +print(f"\n{'='*60}") +print(f"Results: {passed}/{total} passed") +if passed < total: + print(f"FAILED: {total - passed} tests") + sys.exit(1) +else: + print("ALL TESTS PASSED!") + sys.exit(0) diff --git a/core/init.txt b/core/init.txt index a739ccd..22f91aa 100644 --- a/core/init.txt +++ b/core/init.txt @@ -1,5 +1,5 @@ bash /opt/micro_servers/db2/_run.sh -bash /opt/micro_servers/confs/_run.sh +bash /opt/micro_servers/confs2/_run.sh bash /opt/micro_servers/ai_review/_run.sh bash /opt/micro_servers/dnfs2/_run.sh bash /opt/micro_servers/smtp/_run.sh