darra-ethercat-master 2.0.6

商业 EtherCAT 主站协议栈 · 实时内核驱动 · 抖动 1µs · Windows + Linux · 多编程语言 · 全协议 · 支持复杂拓扑 + 热插拔 · ethercat.darra.xyz · Commercial EtherCAT Master protocol stack · Real-time kernel driver · 1µs jitter · Multi-platform · Multi-language · Complex topology + hot-plug.
//! 内核驱动可用性门控. 用法:
//!
//! ```no_run
//! use darra_ethercat::kernel_guard;
//!
//! match kernel_guard::assert_available() {
//!     Ok(()) => { /* master.initialize() / set_network() ... */ }
//!     Err(e) => {
//!         eprintln!("内核驱动不可用: {}", e.message);
//!         kernel_guard::open_installer_website();
//!         std::process::exit(1);
//!     }
//! }
//! ```
//!
//! 不抛错的轻量探测:
//!
//! ```no_run
//! use darra_ethercat::kernel_guard::{self, DarraKernelStatus};
//!
//! match kernel_guard::probe() {
//!     DarraKernelStatus::Ok => { /* 驱动 OK */ }
//!     status => {
//!         let msg = kernel_guard::get_status_message(status);
//!         eprintln!("驱动状态: {:?} - {}", status, msg);
//!     }
//! }
//! ```
//!
//! 该模块对齐 C# `DarraEtherCAT_Master::KernelGuard`, Java `KernelGuard`,
//! Python `darra_ethercat_master.kernel_guard`. 三个 ordinal:
//! `D_1629` (probe), `D_1630` (status message), `D_1631` (installer url).

use std::ffi::CStr;
use std::os::raw::c_char;

use thiserror::Error;

use crate::utils::ffi;

/// 内核驱动 (DarraRT_Eth.sys) 可用性诊断码.
///
/// 与 `utils/kernel_probe.h` 中的 `DarraKernelStatus` 一一对应; SDK 不依赖具体
/// int 值, 应用代码应通过本枚举判断状态.
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DarraKernelStatus {
    /// 内核驱动正常运行
    Ok = 0,
    /// 服务未安装
    NotInstalled = 1,
    /// 服务已安装但未运行
    Stopped = 2,
    /// 没有访问权限 (需要管理员)
    AccessDenied = 3,
    /// 驱动签名校验失败
    SignatureFail = 4,
    /// 被安全软件 / WHQL 策略拦截
    Blocked = 5,
    /// 设备节点缺失 (sys 加载但未创建 \\.\DarraRT_Eth)
    DeviceMissing = 6,
    /// 服务被 SCM 显式禁用
    Disabled = 7,
    /// 未知错误 / DLL 入口缺失
    UnknownError = 99,
}

impl DarraKernelStatus {
    /// 把 native int 还原为枚举; 未识别的值统一映射为 `UnknownError`.
    pub fn from_i32(code: i32) -> Self {
        match code {
            0 => DarraKernelStatus::Ok,
            1 => DarraKernelStatus::NotInstalled,
            2 => DarraKernelStatus::Stopped,
            3 => DarraKernelStatus::AccessDenied,
            4 => DarraKernelStatus::SignatureFail,
            5 => DarraKernelStatus::Blocked,
            6 => DarraKernelStatus::DeviceMissing,
            7 => DarraKernelStatus::Disabled,
            _ => DarraKernelStatus::UnknownError,
        }
    }

    /// 返回 native int 表示 (用于日志 / 跨语言桥接).
    pub fn as_i32(self) -> i32 {
        self as i32
    }
}

impl std::fmt::Display for DarraKernelStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        // 默认走 DLL 中文消息, 用户也可自己 GetStatusMessage 拿
        write!(f, "{}", get_status_message(*self))
    }
}

/// 内核驱动不可用错误 (对齐 C# `DarraKernelNotAvailableException`).
///
/// `message` 是从 DLL 拿到的中文可读说明; `installer_url` 是安装包下载链接.
/// 应用层 catch 后可以选择: 弹消息框 / 打开浏览器 / 调
/// [`open_installer_website`] 一键引导.
#[derive(Error, Debug, Clone)]
#[error("EtherCAT 内核驱动不可用 ({status:?}): {message}\n安装链接: {installer_url}")]
pub struct DarraKernelNotAvailableError {
    /// 具体诊断状态
    pub status: DarraKernelStatus,
    /// DLL 返回的中文说明 (UTF-8)
    pub message: String,
    /// 安装包下载 URL
    pub installer_url: String,
}

/// 内部辅助: 把 DLL 返回的 const char* 转成 String, NULL 时返回 fallback.
fn cstr_to_string(p: *const c_char, fallback: &str) -> String {
    if p.is_null() {
        return fallback.to_string();
    }
    unsafe { CStr::from_ptr(p).to_string_lossy().into_owned() }
}

/// 探测当前内核驱动状态. 不抛错.
///
/// 内部调用 DLL 导出 `D_1629 DarraEcat_KernelProbe`. 对应 C#
/// `KernelGuard.Probe()`.
pub fn probe() -> DarraKernelStatus {
    // 调 DLL; 若 DLL 缺导出 (旧版本), 静态链接阶段就会报错, 走不到运行时.
    let code = unsafe { ffi::DarraEcat_KernelProbe() };
    DarraKernelStatus::from_i32(code as i32)
}

/// 把状态码转成中文可读消息 (从 DLL 拿).
///
/// 对应 C# `KernelGuard.GetStatusMessage()`. NULL 时返回兜底文本.
pub fn get_status_message(status: DarraKernelStatus) -> String {
    let p = unsafe { ffi::DarraEcat_KernelStatusMessage(status.as_i32()) };
    cstr_to_string(p, "EtherCAT 内核驱动状态未知")
}

/// 获取安装包下载链接.
///
/// 默认 `https://www.darrart.com/downloads/drivers`, 部署可通过环境变量
/// `DARRA_INSTALLER_URL` 覆盖. 对应 C# `KernelGuard.GetInstallerUrl()`.
pub fn get_installer_url() -> String {
    let p = unsafe { ffi::DarraEcat_KernelInstallerUrl() };
    cstr_to_string(p, "https://www.darrart.com/downloads/drivers")
}

/// 探测内核状态; 若不可用直接返回 [`DarraKernelNotAvailableError`].
///
/// 应用层应在 `EtherCATMaster::initialize` / `set_network` 之前调用; SDK 内部
/// 门控也会再调一次防御. 对应 C# `KernelGuard.AssertAvailable()`.
pub fn assert_available() -> Result<(), DarraKernelNotAvailableError> {
    let status = probe();
    if status == DarraKernelStatus::Ok {
        return Ok(());
    }
    let message = get_status_message(status);
    let installer_url = get_installer_url();
    Err(DarraKernelNotAvailableError {
        status,
        message,
        installer_url,
    })
}

/// 用默认浏览器打开安装链接. 应用层在收到 [`DarraKernelNotAvailableError`]
/// 后可调用此方法引导用户下载.
///
/// 失败 (浏览器缺失 / 命令执行错误等) 不抛错, 返回 `false`; 不应再次中断
/// 应用流程. 对应 C# `KernelGuard.OpenInstallerWebsite()`.
///
/// - Windows: `cmd /C start "" "<url>"`
/// - macOS  : `open <url>`
/// - Linux  : `xdg-open <url>`
pub fn open_installer_website() -> bool {
    let url = get_installer_url();
    if url.is_empty() {
        return false;
    }
    open_url(&url)
}

#[cfg(target_os = "windows")]
fn open_url(url: &str) -> bool {
    use std::process::Command;
    // cmd.exe start: 第一个引号参数是窗口标题占位符 ("" 让 url 不被当成标题)
    Command::new("cmd")
        .args(["/C", "start", "", url])
        .spawn()
        .is_ok()
}

#[cfg(target_os = "macos")]
fn open_url(url: &str) -> bool {
    use std::process::Command;
    Command::new("open").arg(url).spawn().is_ok()
}

#[cfg(all(unix, not(target_os = "macos")))]
fn open_url(url: &str) -> bool {
    use std::process::Command;
    Command::new("xdg-open").arg(url).spawn().is_ok()
}

#[cfg(not(any(target_os = "windows", target_os = "macos", unix)))]
fn open_url(_url: &str) -> bool {
    false
}