Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions apps/bench/src/workload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,15 +103,16 @@ pub fn generate(env: &GenerateEnv<'_>, plan: &WorkloadPlan) -> io::Result<Worklo
site_id: SITE_ID.to_owned(),
session_id: None,
profiles_dir,
driver: DriverSpec {
driver: Some(DriverSpec {
plugin: env.plugin_path.to_path_buf(),
manifest: ManifestSpec {
id: "modbus-tcp".to_owned(),
name: "Modbus TCP".to_owned(),
version: "0.1.0".to_owned(),
abi: AbiSpec { major: 1, minor: 0 },
},
},
}),
drivers: Default::default(),
devices,
northbound: NorthboundConfig {
mqtt: MqttOptions {
Expand Down
2 changes: 2 additions & 0 deletions apps/collector/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ control = [
driver-read = [
"dep:device-manager",
"dep:driver-loader",
"dep:driver-package",
"dep:driver-sdk",
"dep:poll-engine",
"dep:profile-engine",
Expand Down Expand Up @@ -71,6 +72,7 @@ device-manager = { path = "../../crates/device-manager", optional = true }
diagnostics = { path = "../../crates/diagnostics", optional = true }
domain-model = { path = "../../crates/domain-model", optional = true }
driver-loader = { path = "../../crates/driver-loader", optional = true }
driver-package = { path = "../../crates/driver-package", optional = true }
driver-sdk = { path = "../../crates/driver-sdk", optional = true }
observation-model = { path = "../../crates/observation-model", optional = true }
poll-engine = { path = "../../crates/poll-engine", optional = true }
Expand Down
121 changes: 114 additions & 7 deletions apps/collector/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,23 @@ pub struct CollectorConfig {
#[serde(default = "default_profiles_dir")]
pub profiles_dir: PathBuf,
/// 协议 Driver(Native Plugin,§19/§20:cdylib + Manifest)。
///
/// **Legacy 配置(Transitional,方案 §41.1 / §37.3)**:Runtime V2 起
/// 使用 `drivers:` 段([`CollectorConfig::drivers`],Driver Package
/// 目录扫描)。本字段保留反序列化兼容:启动时若显式提供则打印
/// `COLLECTOR_CONFIG_DRIVER_DEPRECATED` 警告并转换为 synthetic
/// package 注册([`CollectorConfig::legacy_driver_provided`]),下一
/// major 删除。内部以 `Option` 承载以区分"未提供"与"提供了空值";
/// `None` 时 serde 跳过序列化,既有配置文件读写不受影响。
#[serde(default, skip_serializing_if = "Option::is_none")]
pub driver: Option<DriverSpec>,
/// Driver Package 目录(Runtime V2 方案 §8/§37.3)。
///
/// 配置后 Collector 扫描全部目录发现 Driver Package 并注册进
/// [`driver_package`] Registry;此时不得再出现 legacy `driver:` 段
/// (两者互斥,`validate` 强制)。
#[serde(default)]
pub driver: DriverSpec,
pub drivers: DriversOptions,
/// 采集设备清单(§100)。至少一台;domain 缺省时由 Profile 决定。
#[serde(default)]
pub devices: Vec<DeviceSpec>,
Expand Down Expand Up @@ -100,6 +115,40 @@ impl Default for DriverSpec {
}
}

/// Driver Package 部署选项(Runtime V2 方案 §8)。
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DriversOptions {
/// Driver Package 目录列表;每个目录下以 `driver.json` 子目录为
/// 一个 package(`driver_package::scan_directories` 语义,§6.3)。
#[serde(default)]
pub directories: Vec<PathBuf>,
/// 部署级隔离覆盖:driver id → 隔离级别。**只允许调得更严格**
/// (§8/§7:不得低于 Manifest `minimum_isolation`),身份元数据
/// (id/version/abi/artifact)不可覆盖。
#[serde(default)]
pub isolation_overrides: BTreeMap<String, IsolationOverride>,
}

/// 部署级隔离级别(§22;serde snake_case 与 Manifest v2 一致)。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum IsolationOverride {
Shared,
PerDriver,
PerDevice,
}

impl From<IsolationOverride> for driver_package::Isolation {
fn from(v: IsolationOverride) -> Self {
match v {
IsolationOverride::Shared => Self::Shared,
IsolationOverride::PerDriver => Self::PerDriver,
IsolationOverride::PerDevice => Self::PerDevice,
}
}
}

/// 插件 Manifest 声明(§20)。
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
Expand Down Expand Up @@ -602,6 +651,13 @@ impl CollectorConfig {
Ok(config)
}

/// legacy `driver:` 段是否被显式提供(§41.1)。
///
/// `Some(spec)` = 用户显式写了 legacy 段;`None` = 未提供。
pub fn legacy_driver_provided(&self) -> bool {
self.driver.is_some()
}

/// 校验配置合法性;组件级约束与各 crate `validate` 一致,缺省
/// 值在此补齐后统一生效(不静默取默认值)。
pub fn validate(&self) -> Result<(), CollectorError> {
Expand Down Expand Up @@ -680,11 +736,61 @@ impl CollectorConfig {
}
}
}
if self.driver.plugin.as_os_str().is_empty() {
return Err(ConfigError::invalid("driver.plugin", "Driver 插件路径不能为空").into());
// Driver 加载来源二选一(Runtime V2 方案 §37.3):
// - `drivers:`(Package 目录扫描,目标态);`isolation_overrides`
// 只能配合本模式出现;
// - legacy `driver:`(Transitional,§41.1):显式提供时打印
// `COLLECTOR_CONFIG_DRIVER_DEPRECATED` 并由运行时转换为
// synthetic package 注册;与 `drivers.directories` 互斥。
let legacy_driver = self.legacy_driver_provided();
if !self.drivers.directories.is_empty() {
if legacy_driver {
return Err(ConfigError::invalid(
"driver",
"legacy `driver:` 段与 `drivers:` Package 扫描互斥;请删除 legacy 段",
)
.into());
}
if self.drivers.directories.is_empty() {
unreachable!("外层已判非空");
}
for (idx, dir) in self.drivers.directories.iter().enumerate() {
if dir.as_os_str().is_empty() {
return Err(ConfigError::invalid(
"drivers.directories[]",
format!("第 {idx} 个目录为空"),
)
.into());
}
}
for id in self.drivers.isolation_overrides.keys() {
if id.is_empty() {
return Err(ConfigError::invalid(
"drivers.isolation_overrides",
"driver id 不能为空",
)
.into());
}
}
} else if !legacy_driver && !self.drivers.isolation_overrides.is_empty() {
return Err(ConfigError::invalid(
"drivers.isolation_overrides",
"隔离覆盖必须配合 drivers.directories 使用",
)
.into());
}
if self.driver.manifest.id.is_empty() {
return Err(ConfigError::invalid("driver.manifest.id", "Driver 标识不能为空").into());
if legacy_driver {
let driver = self.driver.as_ref().expect("legacy_driver_provided 为真");
if driver.plugin.as_os_str().is_empty() {
return Err(
ConfigError::invalid("driver.plugin", "Driver 插件路径不能为空").into(),
);
}
if driver.manifest.id.is_empty() {
return Err(
ConfigError::invalid("driver.manifest.id", "Driver 标识不能为空").into(),
);
}
}
self.northbound.mqtt.validate()?;
self.poll.validate()?;
Expand Down Expand Up @@ -1021,13 +1127,14 @@ mod tests {
site_id: "plant-a".to_owned(),
session_id: None,
profiles_dir: PathBuf::from("profiles"),
driver: DriverSpec {
driver: Some(DriverSpec {
plugin: PathBuf::from("driver.dll"),
manifest: ManifestSpec {
id: "modbus-tcp".to_owned(),
..Default::default()
},
},
}),
drivers: DriversOptions::default(),
devices: vec![DeviceSpec {
id: "vfd-01".to_owned(),
name: None,
Expand Down
145 changes: 119 additions & 26 deletions apps/collector/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ use std::time::Duration;

use device_manager::{DeviceManager, NativeDriverFactory};
use driver_loader::NativePlugin;
use driver_sdk::DriverManifest;
use observation_model::Device;
use poll_engine::PollScheduler;
use tokio::sync::{mpsc, watch};
Expand Down Expand Up @@ -123,32 +122,90 @@ impl CollectorRuntime {
"Device Profile 已加载"
);

// 2) Load Driver(§19/§20:Native Plugin + Manifest)。
let manifest = DriverManifest {
id: config.driver.manifest.id.clone(),
name: config.driver.manifest.name.clone(),
version: config.driver.manifest.version.clone(),
entry: driver_sdk::abi::ENTRY_SYMBOL.to_owned(),
abi: driver_sdk::manifest::AbiVersion {
major: config.driver.manifest.abi.major,
minor: config.driver.manifest.abi.minor,
},
platforms: vec![],
};
let plugin = Arc::new(
NativePlugin::load(&config.driver.plugin, manifest)
.map_err(|e| CollectorError::Driver(Box::new(e)))?,
);
info!(
component = "collector",
plugin = %config.driver.plugin.display(),
driver_id = %config.driver.manifest.id,
"Driver 已加载"
);
// 2) Load Driver(Runtime V2 方案 §37.3 Multi Driver Registry)。
//
// 二选一(config.validate 已保证互斥):
// - `drivers.directories`:扫描 Driver Package(driver.json 唯一
// 事实来源,§7),同一 Collector 注册多个不同 Driver;
// - legacy `driver:`(Transitional §41.1):打印
// COLLECTOR_CONFIG_DRIVER_DEPRECATED 后按既有单插件路径装配。
let mut factory = NativeDriverFactory::new();
factory
.add_plugin(plugin)
.map_err(|e| CollectorError::Driver(Box::new(e)))?;
if !config.drivers.directories.is_empty() {
for dir in &config.drivers.directories {
let packages = driver_package::scan_directories(std::slice::from_ref(dir))
.map_err(|e| CollectorError::Driver(Box::new(e)))?;
for package in packages {
// 隔离覆盖只允许相同或更严格(§7/§8);身份元数据不可
// 覆盖——此处仅校验方向,实际 Host 拓扑在 Phase 7+ 生效。
if let Some(override_iso) = config.drivers.isolation_overrides.get(package.id())
{
let minimum = package.manifest.runtime.minimum_isolation;
let chosen: driver_package::Isolation = (*override_iso).into();
if chosen < minimum {
return Err(CollectorError::Driver(Box::new(
driver_package::ScanError::Artifact {
path: package.root.clone(),
platform: String::new(),
reason: format!(
"isolation_overrides[{id}]={chosen:?} 低于 Manifest 安全下限 {minimum:?}",
id = package.id()
),
},
)));
}
}
let plugin = Arc::new(
NativePlugin::load(&package.artifact_path, synthetic_manifest(&package))
.map_err(|e| CollectorError::Driver(Box::new(e)))?,
);
factory
.add_plugin(plugin)
.map_err(|e| CollectorError::Driver(Box::new(e)))?;
info!(
component = "collector",
driver_id = %package.id(),
version = %package.version(),
artifact = %package.artifact_path.display(),
"Driver Package 已注册"
);
}
}
let count = config.drivers.directories.len();
info!(
component = "collector",
directories = %config
.drivers
.directories
.iter()
.map(|d| d.display().to_string())
.collect::<Vec<_>>()
.join(","),
%count,
"Driver Package 目录扫描完成"
);
} else if config.legacy_driver_provided() {
warn!(
component = "collector",
code = "COLLECTOR_CONFIG_DRIVER_DEPRECATED",
"legacy `driver:` 配置段已废弃(方案 §41.1):请迁移到 `drivers.directories` \
Package 扫描;本版本仍按单插件路径装配,下一 major 删除"
);
let spec = config.driver.as_ref().expect("legacy_driver_provided 为真");
let manifest = legacy_manifest(spec);
let plugin = Arc::new(
NativePlugin::load(&spec.plugin, manifest)
.map_err(|e| CollectorError::Driver(Box::new(e)))?,
);
info!(
component = "collector",
plugin = %spec.plugin.display(),
driver_id = %spec.manifest.id,
"Driver 已加载(legacy 单插件路径)"
);
factory
.add_plugin(plugin)
.map_err(|e| CollectorError::Driver(Box::new(e)))?;
}

// 3) 构造设备(domain 缺省取 Profile 决定,§100 device.yaml),
// 随后注册绑定 Driver/Profile 并生成读取项(§37)。
Expand Down Expand Up @@ -792,3 +849,39 @@ impl CollectorRuntime {
Ok(())
}
}

/// 由 Driver Package Descriptor 构造 ABI v1 加载用 Manifest(Transitional)。
///
/// Package Manifest(v2)是元数据唯一事实来源(§7);本函数只做字段搬运,
/// 供现有 `NativePlugin::load`(ABI v1 校验路径)消费。Host 路径落地后
/// (Phase 7+)此转换随 direct ABI v1 runtime 一并删除。
fn synthetic_manifest(
package: &driver_package::DriverPackageDescriptor,
) -> driver_sdk::DriverManifest {
driver_sdk::DriverManifest {
id: package.manifest.id.clone(),
name: package.manifest.name.clone(),
version: package.manifest.version.clone(),
entry: driver_sdk::abi::ENTRY_SYMBOL.to_owned(),
abi: driver_sdk::manifest::AbiVersion {
major: package.manifest.abi.major,
minor: package.manifest.abi.minor,
},
platforms: vec![],
}
}

/// legacy `driver:` 段 → 加载用 Manifest(§41.1 兼容路径,下一 major 删除)。
fn legacy_manifest(spec: &crate::config::DriverSpec) -> driver_sdk::DriverManifest {
driver_sdk::DriverManifest {
id: spec.manifest.id.clone(),
name: spec.manifest.name.clone(),
version: spec.manifest.version.clone(),
entry: driver_sdk::abi::ENTRY_SYMBOL.to_owned(),
abi: driver_sdk::manifest::AbiVersion {
major: spec.manifest.abi.major,
minor: spec.manifest.abi.minor,
},
platforms: vec![],
}
}
Loading
Loading