311 lines
11 KiB
Rust
311 lines
11 KiB
Rust
//! 配置读取(与 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(¤t));
|
||
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)
|
||
}
|