Files
chongming-worker-rs/src/config.rs
T
2026-08-31 17:18:03 +08:00

311 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 配置读取(与 Python SDK process.py 完全对位)
//!
//! - 全局配置 `config.toml`:仅读取 `[zenoh].json5_path`(默认 `config/zenoh_config.json5`)
//! - 插件配置 `config/workers/<type>/config.toml` 的 `[workers]` 节:展平 + mqtt_topics
use std::path::Path;
use serde_json::Value;
/// 全局配置(仅 zenoh 会话配置路径)
pub struct GlobalConfig {
pub zenoh_json5_path: String,
}
/// 插件配置([workers] 节,语义对齐 Python load_plugin_config)
pub struct PluginConfig {
pub worker_type: String,
pub module: String,
pub factory: String,
pub cleanup: String,
pub count: usize,
pub load_mode: String,
pub mqtt_topics: Vec<Value>,
/// 展平后的运行时配置(`[workers.config]` 等,供插件自用)
pub runtime: Value,
}
/// 读取全局 config.toml 的 `[zenoh].json5_path`
///
/// `json5_path` 若为相对路径,则基于 config.toml 所在目录解析为绝对路径——这样
/// worker 从任何 CWD 启动(含容器部署)都能定位到与之同目录的 zenoh 会话配置,
/// 与 Python 进程插件"配置自包含在插件根"的约定一致。
pub fn load_global_config(path: &str) -> Result<GlobalConfig, String> {
let cfg_path = std::path::Path::new(path);
let text =
std::fs::read_to_string(cfg_path).map_err(|e| format!("读取全局配置 {path} 失败: {e}"))?;
let cfg: toml::Value =
toml::from_str(&text).map_err(|e| format!("解析全局配置 {path} 失败: {e}"))?;
let json5_path = cfg
.get("zenoh")
.and_then(|z| z.get("json5_path"))
.and_then(toml::Value::as_str)
.unwrap_or("config/zenoh_config.json5");
let base_dir = cfg_path
.parent()
.unwrap_or_else(|| std::path::Path::new("."));
let resolved = std::path::Path::new(json5_path);
let zenoh_json5_path = if resolved.is_absolute() {
resolved.to_path_buf()
} else {
base_dir.join(resolved)
};
Ok(GlobalConfig {
zenoh_json5_path: zenoh_json5_path.to_string_lossy().into_owned(),
})
}
/// 读取 `config/workers/<type>/config.toml` 的 `[workers]` 节并展平
/// (展平规则与 Python SDK load_plugin_config 一致,worker 侧独立实现)
pub fn load_plugin_config(worker_type: &str, config_dir: &str) -> Result<PluginConfig, String> {
let path = Path::new(config_dir).join(worker_type).join("config.toml");
let text = std::fs::read_to_string(&path)
.map_err(|e| format!("未找到插件配置 {}: {e}", path.display()))?;
let plugin_conf: toml::Value =
toml::from_str(&text).map_err(|e| format!("解析插件配置 {} 失败: {e}", path.display()))?;
let workers = plugin_conf
.get("workers")
.and_then(toml::Value::as_table)
.ok_or_else(|| format!("{} 缺少 [workers] 节", path.display()))?;
let plugin_type = workers
.get("type")
.and_then(toml::Value::as_str)
.unwrap_or(worker_type)
.to_string();
let load_mode = workers
.get("load_mode")
.and_then(toml::Value::as_str)
.unwrap_or("inline")
.to_string();
// mqtt_topics:仅保留带 topic 的 dict,并补 worker 字段(与 Python 一致)
let mut mqtt_topics: Vec<Value> = Vec::new();
if let Some(topics) = workers.get("mqtt_topics").and_then(toml::Value::as_array) {
for t in topics {
let has_topic = t.get("topic").map(toml::Value::is_str).unwrap_or(false);
if !has_topic {
continue;
}
let mut t = t.clone();
let has_worker = t.get("worker").map(toml::Value::is_str).unwrap_or(false);
if !has_worker {
if let Some(table) = t.as_table_mut() {
table.insert(
"worker".to_string(),
toml::Value::String(plugin_type.clone()),
);
}
}
mqtt_topics.push(toml_to_json(&t));
}
}
// 展平:插件全量配置 + [workers] 节(除 config/mqtt_topics/load_mode)+ [workers.config]
let mut runtime: toml::Value = plugin_conf.clone();
if let Some(flat) = runtime.as_table_mut() {
for (k, v) in workers {
if k == "config" || k == "mqtt_topics" || k == "load_mode" {
continue;
}
flat.insert(k.clone(), v.clone());
}
}
if let Some(cfg) = workers.get("config") {
if let Some(table) = runtime.as_table_mut() {
table.insert("config".to_string(), cfg.clone());
}
}
// 顶层保留完整 [workers](含 config),与 Python runtime_config 结构一致
let runtime_json = toml_to_json(&runtime);
Ok(PluginConfig {
worker_type: plugin_type,
module: workers
.get("module")
.and_then(toml::Value::as_str)
.unwrap_or(&format!("plugins.{worker_type}"))
.to_string(),
factory: workers
.get("factory")
.and_then(toml::Value::as_str)
.unwrap_or("create_workers")
.to_string(),
cleanup: workers
.get("cleanup")
.and_then(toml::Value::as_str)
.unwrap_or("cleanup")
.to_string(),
count: workers
.get("count")
.and_then(toml::Value::as_integer)
.map(|c| c.max(1) as usize)
.unwrap_or(1),
load_mode,
mqtt_topics,
runtime: runtime_json,
})
}
/// toml::Value → serde_json::Value(插件运行时配置直接以 JSON 暴露)
pub fn toml_to_json(v: &toml::Value) -> Value {
match v {
toml::Value::String(s) => Value::String(s.clone()),
toml::Value::Integer(i) => Value::from(*i),
toml::Value::Float(f) => Value::from(*f),
toml::Value::Boolean(b) => Value::Bool(*b),
toml::Value::Datetime(d) => Value::String(d.to_string()),
toml::Value::Array(a) => Value::Array(a.iter().map(toml_to_json).collect()),
toml::Value::Table(t) => Value::Object(
t.iter()
.map(|(k, v)| (k.clone(), toml_to_json(v)))
.collect(),
),
}
}
// ============================================================
// [workers.config] 运行时读写(对齐 Python runtime.py 的 config 管理)
// ============================================================
/// 禁止通过 config_set 写入的保留键(破坏插件加载/路由语义)
pub const CONFIG_FORBIDDEN_KEYS: &[&str] = &[
"type",
"module",
"factory",
"cleanup",
"count",
"load_mode",
"mqtt_topics",
"workers",
"worker",
"defaults",
"registration",
];
/// 插件 config.toml 路径(config/workers/<type>/config.toml)
pub fn plugin_config_path(worker_type: &str, config_dir: &str) -> std::path::PathBuf {
std::path::Path::new(config_dir)
.join(worker_type)
.join("config.toml")
}
/// 读该类型 `[workers].config` 段(对象),缺失/损坏返回空对象
pub fn read_worker_config_section(worker_type: &str, config_dir: &str) -> Value {
let path = plugin_config_path(worker_type, config_dir);
let text = match std::fs::read_to_string(&path) {
Ok(t) => t,
Err(_) => return Value::Object(Default::default()),
};
let data: toml::Value = toml::from_str(&text).unwrap_or(toml::Value::Table(Default::default()));
match data.get("workers").and_then(toml::Value::as_table) {
Some(w) => match w.get("config") {
Some(cfg) => toml_to_json(cfg),
None => Value::Object(Default::default()),
},
None => Value::Object(Default::default()),
}
}
/// 把 updates 合并写回该类型 `[workers].config`(仅 index0 落盘)。
/// 返回合并后的段;校验保留键失败抛 Err。
pub fn persist_worker_config_section(
worker_type: &str,
config_dir: &str,
updates: &Value,
) -> Result<Value, String> {
if !updates.is_object() {
return Err("config_set 需要 data.config 字段(非空对象)".into());
}
let bad: Vec<String> = updates
.as_object()
.unwrap()
.keys()
.filter(|k| CONFIG_FORBIDDEN_KEYS.contains(&k.as_str()))
.cloned()
.collect();
if !bad.is_empty() {
return Err(format!("禁止修改保留配置键: {bad:?}"));
}
let path = plugin_config_path(worker_type, config_dir);
let current = merge_confirmed(read_worker_config_section(worker_type, config_dir), updates);
// 写回 toml 文件:[workers].config = merged(缺 [workers] 节则创建)
let text = std::fs::read_to_string(&path).unwrap_or_default();
let parsed: toml::Value =
toml::from_str(&text).unwrap_or_else(|_| toml::Value::Table(toml::value::Table::new()));
let mut data: toml::Value = if parsed.as_table().is_some() {
parsed
} else {
toml::Value::Table(toml::value::Table::new())
};
// 定位/创建 [workers] 表
if data
.get("workers")
.and_then(toml::Value::as_table)
.is_none()
{
let t = data.as_table_mut().unwrap();
t.insert(
"workers".into(),
toml::Value::Table(toml::value::Table::new()),
);
}
let workers_tbl = data.get_mut("workers").unwrap().as_table_mut().unwrap();
workers_tbl.insert("config".into(), json_to_toml(&current));
let out = toml::to_string(&data).map_err(|e| format!("序列化 config.toml 失败: {e}"))?;
// 原子写:临时文件 + rename
let tmp = path.with_extension("toml.tmp");
std::fs::write(&tmp, out).map_err(|e| format!("写入配置失败: {e}"))?;
std::fs::rename(&tmp, &path).map_err(|e| format!("原子替换配置失败: {e}"))?;
Ok(current)
}
/// json::Value → toml::Value
pub fn json_to_toml(v: &Value) -> toml::Value {
match v {
Value::Null => toml::Value::String(String::new()),
Value::Bool(b) => toml::Value::Boolean(*b),
Value::Number(n) => {
if let Some(i) = n.as_i64() {
toml::Value::Integer(i)
} else if let Some(f) = n.as_f64() {
toml::Value::Float(f)
} else {
toml::Value::String(n.to_string())
}
}
Value::String(s) => toml::Value::String(s.clone()),
Value::Array(a) => toml::Value::Array(a.iter().map(json_to_toml).collect()),
Value::Object(m) => toml::Value::Table(
m.iter()
.map(|(k, v)| (k.clone(), json_to_toml(v)))
.collect(),
),
}
}
/// 基于 current 合并 updates(过滤保留键)
pub fn merge_confirmed(current: Value, updates: &Value) -> Value {
let mut obj = current.as_object().cloned().unwrap_or_default();
for (k, v) in updates.as_object().unwrap_or(&Default::default()) {
if !CONFIG_FORBIDDEN_KEYS.contains(&k.as_str()) {
obj.insert(k.clone(), v.clone());
}
}
Value::Object(obj)
}
/// 计算 updates 中被允许(非保留键)的键个数
pub fn count_allowed(updates: &Value) -> usize {
updates
.as_object()
.map(|m| {
m.keys()
.filter(|k| !CONFIG_FORBIDDEN_KEYS.contains(&k.as_str()))
.count()
})
.unwrap_or(0)
}