feat: confs2 C++20 重写替代 Python confs,时间戳改系统时区
- 新增 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
This commit is contained in:
1 parent
c13d11aafb
commit
1b282c6f1a
10 files changed
+917
-7
No files matched your search
@@ -0,0 +1,2 @@
|
||||
# configs 符号链接(由 _set.sh 创建,指向 ../confs/configs/)
|
||||
configs
|
||||
@@ -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)
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
cd "$(dirname "$0")" || exit
|
||||
sleep 1
|
||||
exec ./confs2.bin
|
||||
Executable
+13
@@ -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
|
||||
@@ -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 <us/Msg.hpp>
|
||||
#include <us/AsyncQueue.hpp>
|
||||
#include <boost/json.hpp>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
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<pmc::net::AsyncQueue<LogEntry>>(
|
||||
[this](std::vector<LogEntry>&& 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<LogEntry>&&) {
|
||||
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<LogEntry>&& 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<pmc::net::AsyncQueue<LogEntry>> queue_;
|
||||
};
|
||||
|
||||
} // namespace confs2
|
||||
@@ -0,0 +1,589 @@
|
||||
/*
|
||||
* confs2 — C++20 统一配置服务(pymsg RPC)
|
||||
*
|
||||
* 基于 libmsg RpcServer,协议与 Python confs 完全兼容,消费方零改动。
|
||||
* ConfigStore 磁盘缓存 + mtime 自动重载,安全机制逐项 C++ 实现。
|
||||
*/
|
||||
|
||||
#include <us/Msg.hpp>
|
||||
#include <us/RpcServer.hpp>
|
||||
#include <us/RpcClient.hpp>
|
||||
|
||||
#include <boost/json.hpp>
|
||||
#include <boost/program_options.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <regex>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#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<std::string> 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<std::chrono::system_clock::duration>(
|
||||
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<std::chrono::microseconds>(
|
||||
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<long long>(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<std::mutex> 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<char>(f)),
|
||||
std::istreambuf_iterator<char>());
|
||||
|
||||
CacheEntry entry{content, mtime, content.size()};
|
||||
cache_[name] = entry;
|
||||
return entry; // 返回拷贝
|
||||
}
|
||||
|
||||
// 列出配置目录全部文件元信息
|
||||
std::vector<ConfigMeta> list() {
|
||||
std::vector<ConfigMeta> out;
|
||||
std::lock_guard<std::mutex> lock(mtx_);
|
||||
std::string root_str = root_.string();
|
||||
|
||||
std::vector<std::string> 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<std::string> reload(const std::string* name) {
|
||||
if (name != nullptr) {
|
||||
validate_name(*name);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx_);
|
||||
resolve_locked(*name); // 锁内检查存在性,防止 TOCTOU
|
||||
cache_.erase(*name);
|
||||
}
|
||||
get(*name); // 重新读取并缓存(get 内部自己加锁)
|
||||
return {*name};
|
||||
}
|
||||
// 清空全部缓存
|
||||
std::vector<std::string> dropped;
|
||||
std::lock_guard<std::mutex> 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<std::mutex> 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<std::string, CacheEntry> 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<std::string> 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<std::string> 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<std::chrono::seconds>(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<std::string>(), "监听地址")
|
||||
("port,p", po::value<int>(), "监听端口")
|
||||
("workers,w", po::value<int>(), "工作线程数")
|
||||
("dir,d", po::value<std::string>(), "配置文件目录");
|
||||
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<std::string>();
|
||||
if (vm.count("port")) g_config.port = vm["port"].as<int>();
|
||||
if (vm.count("workers")) g_config.workers = vm["workers"].as<int>();
|
||||
if (vm.count("dir")) g_config.config_dir = vm["dir"].as<std::string>();
|
||||
|
||||
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<std::string>(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<std::string>(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<std::string>(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;
|
||||
}
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user