From 5636c8e1d93a57ecf169f2c684d70ec1173f0305 Mon Sep 17 00:00:00 2001 From: shenjack <3695888@qq.com> Date: Sun, 18 Aug 2024 00:11:13 +0800 Subject: [PATCH] =?UTF-8?q?=E7=96=91=E4=BC=BC=E6=9C=89=E7=82=B9=E8=BF=87?= =?UTF-8?q?=E4=BA=8E=E4=BB=A3=E7=A0=81=E5=A4=9A=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 17 +++--- ica-rs/Cargo.toml | 1 + ica-rs/src/py/call.rs | 28 +++++---- ica-rs/src/py/config.rs | 108 +++++++++++++++++++++++++++++++++ ica-rs/src/py/mod.rs | 131 +++++++++++++++++++++++++++++++--------- 5 files changed, 236 insertions(+), 49 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d6df514..319bcec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -678,6 +678,7 @@ dependencies = [ "thiserror", "tokio", "toml", + "toml_edit", "tracing", "tracing-subscriber", ] @@ -1391,9 +1392,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" dependencies = [ "serde", ] @@ -1713,18 +1714,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.13" +version = "0.22.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" dependencies = [ "indexmap", "serde", @@ -2197,9 +2198,9 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" -version = "0.6.9" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86c949fede1d13936a99f14fafd3e76fd642b556dd2ce96287fbe2e0151bfac6" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" dependencies = [ "memchr", ] diff --git a/ica-rs/Cargo.toml b/ica-rs/Cargo.toml index 4079a8d..717b699 100644 --- a/ica-rs/Cargo.toml +++ b/ica-rs/Cargo.toml @@ -51,3 +51,4 @@ anyhow = { version = "1.0", features = ["backtrace"] } tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["time"] } thiserror = "1.0.63" +toml_edit = "0.22.20" diff --git a/ica-rs/src/py/call.rs b/ica-rs/src/py/call.rs index 0a96f17..d126378 100644 --- a/ica-rs/src/py/call.rs +++ b/ica-rs/src/py/call.rs @@ -60,7 +60,7 @@ pub fn get_func<'py>( } } -pub fn verify_plugins() { +pub fn verify_and_reload_plugins() { let mut need_reload_files: Vec = Vec::new(); let plugin_path = MainStatus::global_config().py().plugin_path.clone(); @@ -77,14 +77,20 @@ pub fn verify_plugins() { return; } info!("file change list: {:?}", need_reload_files); + let exist_plugins = PyStatus::get_map_mut(); for reload_file in need_reload_files { - match PyPlugin::new_from_path(&reload_file) { - Some(plugin) => { - PyStatus::add_file(reload_file.clone(), plugin); - info!("重载 Python 插件: {:?}", reload_file); - } - None => { - warn!("重载 Python 插件: {:?} 失败", reload_file); + if let Some(plugin) = exist_plugins.get_mut(&reload_file) { + plugin.reload_from_file(); + event!(Level::INFO, "重载 Python 插件: {:?} 完成", reload_file); + } else { + match PyPlugin::new_from_path(&reload_file) { + Some(plugin) => { + PyStatus::add_file(reload_file.clone(), plugin); + info!("加载 Python 插件: {:?} 完成", reload_file); + } + None => { + warn!("加载 Python 插件: {:?} 失败", reload_file); + } } } } @@ -133,7 +139,7 @@ macro_rules! call_py_func { /// 执行 new message 的 python 插件 pub async fn ica_new_message_py(message: &ica::messages::NewMessage, client: &Client) { // 验证插件是否改变 - verify_plugins(); + verify_and_reload_plugins(); let plugins = PyStatus::get_map(); for (path, plugin) in plugins.iter().filter(|(_, plugin)| plugin.enabled) { @@ -146,7 +152,7 @@ pub async fn ica_new_message_py(message: &ica::messages::NewMessage, client: &Cl } pub async fn ica_delete_message_py(msg_id: ica::MessageId, client: &Client) { - verify_plugins(); + verify_and_reload_plugins(); let plugins = PyStatus::get_map(); for (path, plugin) in plugins.iter().filter(|(_, plugin)| plugin.enabled) { @@ -161,7 +167,7 @@ pub async fn tailchat_new_message_py( message: &tailchat::messages::ReceiveMessage, client: &Client, ) { - verify_plugins(); + verify_and_reload_plugins(); let plugins = PyStatus::get_map(); for (path, plugin) in plugins.iter().filter(|(_, plugin)| plugin.enabled) { diff --git a/ica-rs/src/py/config.rs b/ica-rs/src/py/config.rs index 8b13789..e9ea529 100644 --- a/ica-rs/src/py/config.rs +++ b/ica-rs/src/py/config.rs @@ -1 +1,109 @@ +use std::{path::{Path, PathBuf}, str::FromStr}; +use toml_edit::{value, DocumentMut, Key, Table, TomlError, Value}; + +use crate::py::PyStatus; + +/// ```toml +/// # 这个文件是由 shenbot 自动生成的, 请 **谨慎** 修改 +/// # 请不要修改这个文件, 除非你知道你在做什么 +/// +/// [plugins] +/// "xxxxxxx" = false # 被禁用的插件 +/// "xxxxxxx" = true # 被启用的插件 +/// ``` +#[derive(Debug, Clone)] +pub struct PluginConfigFile { + pub data: DocumentMut, +} + +const CONFIG_KEY: &str = "plugins"; +pub const CONFIG_FILE_NAME: &str = "/plugins.toml"; +pub const DEFAULT_CONFIG: &str = r#" +# 这个文件是由 shenbot 自动生成的, 请 **谨慎** 修改 +# 请不要修改这个文件, 除非你知道你在做什么 +[plugins] +"#; + +impl PluginConfigFile { + pub fn from_str(data: &str) -> Result { + let data = DocumentMut::from_str(data)?; + Ok(Self { data }) + } + + pub fn from_config_path(path: &Path) -> anyhow::Result { + let config_path = path.join(CONFIG_FILE_NAME); + if !config_path.exists() { + std::fs::write(&config_path, DEFAULT_CONFIG)?; + Ok(Self::from_str(DEFAULT_CONFIG)?) + } else { + let data = std::fs::read_to_string(&config_path)?; + Ok(Self::from_str(&data)?) + } + } + + pub fn verify_and_init(&mut self) { + if self.data.get(CONFIG_KEY).is_none() { + self.data.insert_formatted( + &Key::from_str(CONFIG_KEY).unwrap(), + toml_edit::Item::Table(Table::new()), + ); + } + } + + /// 获取插件状态 + /// 默认为 true + pub fn get_status(&self, path: &Path) -> bool { + let path_str = path.to_str().unwrap(); + if let Some(item) = self.data.get(CONFIG_KEY) { + if let Some(table) = item.as_table() { + if let Some(item) = table.get(path_str) { + if let Some(bool) = item.as_bool() { + return bool; + } + } + } + } + true + } + + /// 设置插件状态 + pub fn set_status(&mut self, path: &Path, status: bool) { + self.verify_and_init(); + let path_str = path.to_str().unwrap(); + let table = self.data.get_mut(CONFIG_KEY).unwrap().as_table_mut().unwrap(); + if table.contains_key(path_str) { + match table.get_mut(path_str).unwrap().as_value_mut() { + Some(value) => *value = Value::from(status), + None => { + table.insert(path_str, value(status)); + } + } + } else { + table.insert(path_str, value(status)); + } + } + + pub fn sync_status_from_config(&mut self) { + let plugins = PyStatus::get_map_mut(); + self.verify_and_init(); + plugins.iter_mut().for_each(|(path, status)| { + status.enabled = self.get_status(path); + }); + } + + pub fn sync_status_to_config(&mut self) { + let plugins = PyStatus::get_map(); + self.verify_and_init(); + let table = self.data.get_mut(CONFIG_KEY).unwrap().as_table_mut().unwrap(); + table.clear(); + plugins.iter().for_each(|(path, status)| { + table.insert(path.to_str().unwrap(), value(status.enabled)); + }); + } + + pub fn write_to_file(&self, path: &PathBuf) -> Result<(), std::io::Error> { + std::fs::write(path, self.data.to_string())?; + Ok(()) + } +} diff --git a/ica-rs/src/py/mod.rs b/ica-rs/src/py/mod.rs index c0ed960..5cc3e07 100644 --- a/ica-rs/src/py/mod.rs +++ b/ica-rs/src/py/mod.rs @@ -2,7 +2,6 @@ pub mod call; pub mod class; pub mod config; -use std::fmt::Display; use std::path::Path; use std::time::SystemTime; use std::{collections::HashMap, path::PathBuf}; @@ -10,13 +9,14 @@ use std::{collections::HashMap, path::PathBuf}; use colored::Colorize; use pyo3::prelude::*; use pyo3::types::PyTuple; -use tracing::{debug, event, info, span, warn, Level}; +use tracing::{event, info, span, warn, Level}; use crate::MainStatus; #[derive(Debug, Clone)] pub struct PyStatus { pub files: Option, + pub config: Option, } pub type PyPlugins = HashMap; @@ -28,16 +28,24 @@ impl PyStatus { if PYSTATUS.files.is_none() { PYSTATUS.files = Some(HashMap::new()); } + if PYSTATUS.config.is_none() { + let plugin_path = MainStatus::global_config().py().plugin_path; + let mut config = + config::PluginConfigFile::from_config_path(&PathBuf::from(plugin_path)) + .unwrap(); + config.verify_and_init(); + PYSTATUS.config = Some(config); + } } } - pub fn add_file(path: PathBuf, plugin: PyPlugin) { Self::get_mut_map().insert(path, plugin); } + pub fn add_file(path: PathBuf, plugin: PyPlugin) { Self::get_map_mut().insert(path, plugin); } pub fn verify_file(path: &PathBuf) -> bool { Self::get_map().get(path).map_or(false, |plugin| plugin.verifiy()) } - fn get_map() -> &'static PyPlugins { + pub fn get_map() -> &'static PyPlugins { unsafe { match PYSTATUS.files.as_ref() { Some(files) => files, @@ -49,7 +57,7 @@ impl PyStatus { } } - fn get_mut_map() -> &'static mut PyPlugins { + pub fn get_map_mut() -> &'static mut PyPlugins { unsafe { match PYSTATUS.files.as_mut() { Some(files) => files, @@ -61,11 +69,43 @@ impl PyStatus { } } - pub fn list_plugins() -> Vec { Self::get_map().keys().cloned().collect() } + pub fn get_config() -> &'static config::PluginConfigFile { + unsafe { + match PYSTATUS.config.as_ref() { + Some(config) => config, + None => { + Self::init(); + PYSTATUS.config.as_ref().unwrap() + } + } + } + } + + pub fn get_config_mut() -> &'static mut config::PluginConfigFile { + unsafe { + match PYSTATUS.config.as_mut() { + Some(config) => config, + None => { + Self::init(); + PYSTATUS.config.as_mut().unwrap() + } + } + } + } + + pub fn get_status(path: &Path) -> bool { Self::get_config().get_status(path) } + + // pub fn list_plugins() -> Vec { Self::get_map().keys().cloned().collect() } pub fn display() -> String { let map = Self::get_map(); - format!("Python 插件 {{ {} }}", (",".join(map.keys().map(|x| x.to_string())))) + format!( + "Python 插件 {{ {} }}", + map.iter() + .map(|(k, v)| format!("{:?}: {:?}", k, v)) + .collect::>() + .join(", ") + ) } } @@ -90,6 +130,7 @@ pub struct PyPlugin { } impl PyPlugin { + /// 从文件创建一个新的 pub fn new_from_path(path: &PathBuf) -> Option { let raw_file = load_py_file(path); match raw_file { @@ -111,6 +152,34 @@ impl PyPlugin { } } } + + /// 从文件更新 + pub fn reload_from_file(&mut self) { + let raw_file = load_py_file(&self.file_path); + match raw_file { + Ok(raw_file) => match Self::try_from(raw_file) { + Ok(plugin) => { + self.py_module = plugin.py_module; + self.changed_time = plugin.changed_time; + self.enabled = PyStatus::get_status(self.file_path.as_path()); + event!(Level::INFO, "更新 Python 插件文件 {:?} 完成", self.file_path); + } + Err(e) => { + warn!( + "更新 Python 插件文件{:?}: {:?} 失败\n{}", + self.file_path, + e, + get_py_err_traceback(&e) + ); + } + }, + Err(e) => { + warn!("更新插件 {:?}: {:?} 失败", self.file_path, e); + } + } + } + + /// 检查文件是否被修改 pub fn verifiy(&self) -> bool { match get_change_time(&self.file_path) { None => false, @@ -175,29 +244,26 @@ impl TryFrom for PyPlugin { } let py_config = py_config.unwrap(); // 先判定一下原来有没有 - match module.hasattr(CONFIG_DATA_NAME) { - Ok(true) => { - // get 过来, 后面直接覆盖, 这里用于发个警告 - match module.getattr(CONFIG_DATA_NAME) { - Ok(old_config) => { - // 先判断是不是 None, 直接忽略掉 None - // 毕竟有可能有占位 - if !old_config.is_none() { - warn!( - "Python 插件 {:?} 的配置文件信息已经存在\n原始内容: {}", - path, old_config - ); - } - } - Err(e) => { + if let Ok(true) = module.hasattr(CONFIG_DATA_NAME) { + // get 过来, 后面直接覆盖, 这里用于发个警告 + match module.getattr(CONFIG_DATA_NAME) { + Ok(old_config) => { + // 先判断是不是 None, 直接忽略掉 None + // 毕竟有可能有占位 + if !old_config.is_none() { warn!( - "Python 插件 {:?} 的配置文件信息已经存在, 但获取失败:{:?}", - path, e + "Python 插件 {:?} 的配置文件信息已经存在\n原始内容: {}", + path, old_config ); } } + Err(e) => { + warn!( + "Python 插件 {:?} 的配置文件信息已经存在, 但获取失败:{:?}", + path, e + ); + } } - _ => {} } match module.setattr(CONFIG_DATA_NAME, py_config) { Ok(()) => Ok(PyPlugin { @@ -266,11 +332,14 @@ impl TryFrom for PyPlugin { } } -pub static mut PYSTATUS: PyStatus = PyStatus { files: None }; +pub static mut PYSTATUS: PyStatus = PyStatus { + files: None, + config: None, +}; pub fn load_py_plugins(path: &PathBuf) { if path.exists() { - event!(Level::INFO, "finding plugins in: {:?}", path); + event!(Level::INFO, "找到位于 {:?} 的插件", path); // 搜索所有的 py 文件 和 文件夹单层下面的 py 文件 match path.read_dir() { Err(e) => { @@ -293,6 +362,7 @@ pub fn load_py_plugins(path: &PathBuf) { } else { event!(Level::WARN, "插件加载目录不存在: {:?}", path); } + PyStatus::get_config_mut().sync_status_from_config(); event!( Level::INFO, "python 插件目录: {:?} 加载完成, 加载到 {} 个插件", @@ -328,15 +398,16 @@ pub fn load_py_file(path: &PathBuf) -> std::io::Result { /// Python 侧初始化 pub fn init_py() { // 从 全局配置中获取 python 插件路径 - let span = span!(Level::INFO, "Init Python Plugin"); + let span = span!(Level::INFO, "初始化 python 及其插件.ing"); let _enter = span.enter(); - let global_config = MainStatus::global_config().py(); + let plugin_path = MainStatus::global_config().py().plugin_path; event!(Level::INFO, "正在初始化 python"); pyo3::prepare_freethreaded_python(); - let plugin_path = PathBuf::from(global_config.plugin_path); + PyStatus::init(); + let plugin_path = PathBuf::from(plugin_path); load_py_plugins(&plugin_path); event!(Level::DEBUG, "python 插件列表: {}", PyStatus::display());