refactor(core,shim): 统一基于 thiserror 的错误体系
- 移除 anyhow,使用 thiserror 定义强类型 Error 枚举 - 支持 #[from] 自动转换 mlua::Error,关键路径保留源码与上下文 - LuaRuntimeBuilder 采用惰性错误收集,简化链式初始化调用 - MirrorRegistry 实现规范化 FromLuaConversionError 错误转换
This commit is contained in:
@@ -14,3 +14,4 @@ serde = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
tracing-appender = { workspace = true }
|
||||
thiserror = "2.0.20"
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
use std::path::PathBuf;
|
||||
use thiserror::Error;
|
||||
// 构造一个带上下文的 FromLua 转换错误,便于定位配置问题
|
||||
/// 配置校验错误
|
||||
pub fn validation_error(message: impl Into<String>) -> mlua::Error {
|
||||
mlua::Error::RuntimeError(message.into())
|
||||
}
|
||||
pub fn conversion_err(
|
||||
from: &'static str,
|
||||
to: impl Into<String>,
|
||||
msg: impl Into<String>,
|
||||
) -> mlua::Error {
|
||||
mlua::Error::FromLuaConversionError {
|
||||
from,
|
||||
to: to.into(),
|
||||
message: Some(msg.into()),
|
||||
}
|
||||
}
|
||||
/// 将底层 Lua 语法错误转换为用户友好的提示
|
||||
pub fn syntax_error(err: mlua::Error) -> mlua::Error {
|
||||
match &err {
|
||||
@@ -22,4 +35,57 @@ pub fn syntax_error(err: mlua::Error) -> mlua::Error {
|
||||
_ => {}
|
||||
}
|
||||
err
|
||||
}
|
||||
}
|
||||
|
||||
/// 全局统一的 `Result` 别名,默认错误类型为 [`Error`]。
|
||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("初始化 Lua 沙箱失败: {0}")]
|
||||
LuaInit(#[source] mlua::Error),
|
||||
#[error("构建 LuaRuntime 异常")]
|
||||
BuildError,
|
||||
|
||||
///读取脚本文件失败
|
||||
#[error("读取脚本文件失败 ({path}): {source}")]
|
||||
ScriptRead {
|
||||
path: PathBuf,
|
||||
#[source]
|
||||
source: std::io::Error,
|
||||
},
|
||||
|
||||
#[error("脚本 {path} 不是有效的 UTF-8 文件(请将 Lua 配置文件保存为 UTF-8 编码)")]
|
||||
InvalidUtf8 { path: PathBuf },
|
||||
|
||||
// 收敛所有来自 mlua 的错误,附带来源上下文
|
||||
#[error("Lua 执行异常 ({chunk}): {source}")]
|
||||
LuaRuntime {
|
||||
chunk: String,
|
||||
#[source]
|
||||
source: mlua::Error,
|
||||
},
|
||||
|
||||
#[error("模块 '{module_name}' 必须返回一个函数作为入口,实际返回了 {actual_type}")]
|
||||
InvalidModuleEntry {
|
||||
module_name: String,
|
||||
actual_type: &'static str,
|
||||
},
|
||||
|
||||
// #[error("插件/上下文挂载失败 '{name}': {source}")]
|
||||
// Plugin {
|
||||
// name: String,
|
||||
// #[source]
|
||||
// source: mlua::Error,
|
||||
// },
|
||||
|
||||
#[error("Lua 运行时未指定上下文错误: {0}")]
|
||||
LuaError(#[from] mlua::Error),
|
||||
|
||||
// #[error("设置 {0} 环境变量失败")]
|
||||
// SetMirrorDir(String),
|
||||
|
||||
/// 用户自定义扩展错误,用于包装第三方插件产生的错误。
|
||||
#[error("用户自定义扩展错误: {0}")]
|
||||
Other(#[source] Box<dyn std::error::Error + Send + Sync>),
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod error;
|
||||
mod error;
|
||||
mod layout;
|
||||
mod logger;
|
||||
mod plugins;
|
||||
mod runtime;
|
||||
mod utils;
|
||||
mod validators;
|
||||
@@ -9,6 +10,9 @@ pub use layout::Layout;
|
||||
// pub use mirror_shim::mirror::Mirror;
|
||||
pub use runtime::LuaRuntime;
|
||||
|
||||
pub use logger::{init_logging_from, LogLevel, Logger};
|
||||
// pub use mirror_shim::utils::{lua_string_2_os_string, normalize_path_for_lua, parse_tokens};
|
||||
pub use validators::validate_command_name;
|
||||
pub use logger::{LogConfig, LogLevel, init_logging_from};
|
||||
pub use plugins::register_basics;
|
||||
pub use utils::normalize_path_for_lua;
|
||||
pub use validators::validate_command_name;
|
||||
// 把 Result 和 Error 提升到根命名空间,给外部带来极致便利
|
||||
pub use error::{Error, Result,validation_error,conversion_err};
|
||||
|
||||
+65
-63
@@ -1,7 +1,7 @@
|
||||
use crate::Layout;
|
||||
use serde::Deserialize;
|
||||
use std::convert::Infallible;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use tracing_appender::non_blocking::{NonBlocking, WorkerGuard};
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
@@ -61,41 +61,50 @@ impl FromStr for LogLevel {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
pub struct Logger {
|
||||
#[serde(default)]
|
||||
/// 日志配置模型(对应 mirror.ini 的纯强类型表示)
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct LogConfig {
|
||||
pub level: LogLevel,
|
||||
pub log_dir: Option<PathBuf>,
|
||||
}
|
||||
impl Logger {
|
||||
impl LogConfig {
|
||||
/// 零依赖解析 ini / key-value 文本内容
|
||||
pub fn parse_ini(content: &str) -> Self {
|
||||
let mut logger = Self::default();
|
||||
/// 计算日志目录的实际物理路径(相对路径基于 base_dir 拼接)
|
||||
pub fn resolve_log_dir(&self, base_dir: &Path) -> PathBuf {
|
||||
match &self.log_dir {
|
||||
Some(p) if p.is_absolute() => p.clone(),
|
||||
Some(p) => base_dir.join(p),
|
||||
None => base_dir.join("logs"),
|
||||
}
|
||||
}
|
||||
}
|
||||
// 协议 1: 反序列化 (INI 文本 -> LogConfig)
|
||||
impl FromStr for LogConfig {
|
||||
type Err = Infallible;
|
||||
|
||||
fn from_str(content: &str) -> Result<Self, Self::Err> {
|
||||
let mut config = Self::default();
|
||||
|
||||
for line in content.lines() {
|
||||
let line = line.trim();
|
||||
// 跳过空行、注释行 (# 或 ;) 和 section 头 ([...])
|
||||
if line.is_empty()
|
||||
|| line.starts_with('#')
|
||||
|| line.starts_with(';')
|
||||
|| line.starts_with('[')
|
||||
{
|
||||
// 跳过空行、注释 (#, ;) 与 section 标头 ([...])
|
||||
if line.is_empty() || line.starts_with(['#', ';', '[']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some((key, val)) = line.split_once('=') {
|
||||
let key = key.trim();
|
||||
let val = val.trim().trim_matches('"').trim_matches('\'');
|
||||
let val = val.trim().trim_matches(['"', '\'']);
|
||||
|
||||
match key {
|
||||
"level" => {
|
||||
if let Ok(lvl) = LogLevel::from_str(val) {
|
||||
logger.level = lvl;
|
||||
config.level = lvl;
|
||||
}
|
||||
}
|
||||
"log_dir" => {
|
||||
if !val.is_empty() {
|
||||
logger.log_dir = Some(PathBuf::from(val));
|
||||
config.log_dir = Some(PathBuf::from(val));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -103,9 +112,24 @@ impl Logger {
|
||||
}
|
||||
}
|
||||
|
||||
logger
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
// 协议 2: 序列化 (LogConfig -> INI 文本)
|
||||
impl std::fmt::Display for LogConfig {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
let dir_str = self
|
||||
.log_dir
|
||||
.as_deref()
|
||||
.and_then(|p| p.to_str())
|
||||
.unwrap_or("logs");
|
||||
|
||||
writeln!(f, "level = \"{}\"", self.level.as_str())?;
|
||||
writeln!(f, "log_dir = \"{}\"", dir_str)
|
||||
}
|
||||
}
|
||||
|
||||
/// 基于 Layout 自动寻址 mirror.toml 并初始化日志系统。
|
||||
/// 仅在开启详细日志(如 info/debug/trace)时激活异步文件记录并返回 `WorkerGuard`。
|
||||
pub fn init_logging_from(layout: &Layout) -> Option<WorkerGuard> {
|
||||
@@ -113,16 +137,11 @@ pub fn init_logging_from(layout: &Layout) -> Option<WorkerGuard> {
|
||||
// let config_path = layout.base_dir.join("mirror.toml");
|
||||
let config_path = layout.base_dir.join("mirror.ini");
|
||||
|
||||
// 读取配置文件(如不存在或解析失败,降级回退到默认设置)
|
||||
let config = if config_path.exists() {
|
||||
fs::read_to_string(&config_path)
|
||||
.map(|s| Logger::parse_ini(&s))
|
||||
// .ok()
|
||||
// .and_then(|s| toml::from_str::<Logger>(&s).ok())
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
Logger::default()
|
||||
};
|
||||
// 1. 读取并反序列化配置文件(缺失或异常时安全降级为 Default)
|
||||
let config: LogConfig = fs::read_to_string(&config_path)
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
// 1. 优先读取环境变量 MIRROR_LOG_LEVEL,解析失败或未设置则退回到 config.level
|
||||
let effective_level = std::env::var("MIRROR_LOG_LEVEL")
|
||||
@@ -136,47 +155,30 @@ pub fn init_logging_from(layout: &Layout) -> Option<WorkerGuard> {
|
||||
// 基础终端输出层
|
||||
let stderr_layer = fmt::layer().with_writer(std::io::stderr).with_target(false);
|
||||
|
||||
// 2. 根据强类型枚举判断是否激活落盘模式
|
||||
if effective_level.is_verbose() {
|
||||
let (non_blocking, guard) = create_file_appender(layout, config.log_dir);
|
||||
let file_layer = fmt::layer()
|
||||
// 3. 动态配置落盘文件层(仅在 verbose 级别启用)
|
||||
let (file_layer, guard) = if effective_level.is_verbose() {
|
||||
let (non_blocking, guard) = create_file_appender(&config.resolve_log_dir(&layout.base_dir));
|
||||
let layer = fmt::layer()
|
||||
.with_writer(non_blocking)
|
||||
.with_ansi(false)
|
||||
.with_target(false);
|
||||
|
||||
let _ = tracing_subscriber::registry()
|
||||
.with(filter)
|
||||
.with(stderr_layer)
|
||||
.with(file_layer)
|
||||
.try_init();
|
||||
|
||||
Some(guard)
|
||||
(Some(layer), Some(guard))
|
||||
} else {
|
||||
let _ = tracing_subscriber::registry()
|
||||
.with(filter)
|
||||
.with(stderr_layer)
|
||||
.try_init();
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn create_file_appender(
|
||||
layout: &Layout,
|
||||
log_dir_override: Option<PathBuf>,
|
||||
) -> (NonBlocking, WorkerGuard) {
|
||||
let output_dir = match log_dir_override {
|
||||
Some(user_path) => {
|
||||
if user_path.is_relative() {
|
||||
layout.base_dir.join(user_path)
|
||||
} else {
|
||||
user_path
|
||||
}
|
||||
}
|
||||
None => layout.base_dir.join("logs"),
|
||||
(None, None)
|
||||
};
|
||||
|
||||
if let Err(err) = fs::create_dir_all(&output_dir) {
|
||||
// 4. 统一组装并初始化 Subscriber(Option<Layer> 天然实现了 Layer trait)
|
||||
let _ = tracing_subscriber::registry()
|
||||
.with(filter)
|
||||
.with(stderr_layer)
|
||||
.with(file_layer)
|
||||
.try_init();
|
||||
|
||||
guard
|
||||
}
|
||||
|
||||
fn create_file_appender(output_dir: &Path) -> (NonBlocking, WorkerGuard) {
|
||||
if let Err(err) = fs::create_dir_all(output_dir) {
|
||||
eprintln!("[WARN] 创建日志目录 {} 失败: {}", output_dir.display(), err);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
use crate::error::{Error, Result};
|
||||
use crate::{Layout, normalize_path_for_lua};
|
||||
use mlua::{Lua, Table, Value};
|
||||
use std::env;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
const MIRROR_DIR: &str = "__MIRROR_DIR__";
|
||||
|
||||
// 采用形式 A:直接传引用的普通函数
|
||||
pub fn register_basics(lua: &Lua, layout: &Layout) -> Result<()> {
|
||||
let base_dir = normalize_path_for_lua(&layout.base_dir);
|
||||
register_mirror_dir(lua, &base_dir)?;
|
||||
register_get_env(lua)?;
|
||||
setup_require(lua, &base_dir)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 注入全局锚点变量
|
||||
pub fn register_mirror_dir(lua: &Lua, base_dir: &str) -> Result<()> {
|
||||
debug!("设置 {MIRROR_DIR} 环境变量");
|
||||
lua.globals().set(MIRROR_DIR, base_dir)?;
|
||||
// .context(format!("设置 {MIRROR_DIR} 环境变量失败"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///注入 get_env 供配置读取环境变量
|
||||
pub fn register_get_env(lua: &Lua) -> Result<()> {
|
||||
let globals = lua.globals();
|
||||
let get_env = lua.create_function(|lua, key: String| -> mlua::Result<Table> {
|
||||
// 缺失变量视为空字符串,拆分后得到空表(不贡献任何路径段)
|
||||
let value = env::var_os(key).unwrap_or_default();
|
||||
// 空输入返回空表;否则按平台分隔符拆分(split_paths 会剥离引号包裹)
|
||||
if value.is_empty() {
|
||||
return lua.create_table();
|
||||
}
|
||||
let table = lua.create_table()?;
|
||||
for (i, p) in env::split_paths(&value).enumerate() {
|
||||
// 3. 跨平台提取原始字节并转为 LuaString,保证 100% 无损
|
||||
#[cfg(unix)]
|
||||
let lua_str = {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
lua.create_string(p.as_os_str().as_bytes())?
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
let lua_str = {
|
||||
// Windows 路径是 UTF-16,转成字符串或保持其字节表达
|
||||
let s = p.to_string_lossy();
|
||||
lua.create_string(s.as_bytes())?
|
||||
};
|
||||
|
||||
table.set(i + 1, lua_str)?;
|
||||
}
|
||||
|
||||
Ok(table)
|
||||
})?;
|
||||
// .context("注册 get_env 函数失败")?;
|
||||
|
||||
globals.set("get_env", get_env)?;
|
||||
// .context("挂载 get_env 全局函数失败")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 配置 package.path 并包装 require,拦截加载失败以提高容错性
|
||||
pub fn setup_require(lua: &Lua, base_dir: &str) -> Result<()> {
|
||||
let globals = lua.globals();
|
||||
|
||||
// 1. 安全加固并拓展 package 搜索路径
|
||||
let package = globals.get::<Table>("package")?;
|
||||
|
||||
let _ = package.set("cpath", "");
|
||||
let _ = package.set("loadlib", Value::Nil);
|
||||
|
||||
let commands_dir = format!("{}/commands", base_dir);
|
||||
let vendor_dir = format!("{}/vendor", base_dir);
|
||||
|
||||
let path = package.get::<String>("path")?;
|
||||
let new_path = format!(
|
||||
"{};{}/?.lua;{}/?/init.lua;{}/?.lua;{}/?/init.lua;{}/?.lua;{}/?/init.lua;",
|
||||
path, base_dir, base_dir, commands_dir, commands_dir, vendor_dir, vendor_dir
|
||||
);
|
||||
|
||||
debug!("重新写入的 package 查找路径");
|
||||
package.set("path", new_path)?;
|
||||
|
||||
// 2. 获取原生 require 并通过闭包直接持有(无需向全局表注入备份变量)
|
||||
let original_require: mlua::Function = globals.get("require")?;
|
||||
// .context("获取内置 require 函数失败")?;
|
||||
|
||||
let wrapped_require =
|
||||
lua.create_function(move |_lua, module: String| -> mlua::Result<Value> {
|
||||
match original_require.call::<Value>(module.as_str()) {
|
||||
Ok(value) => Ok(value),
|
||||
Err(e) => {
|
||||
warn!(
|
||||
module = %module,
|
||||
error = %e,
|
||||
"配置模块加载失败,已跳过该条目(可在独立配置文件中定义)"
|
||||
);
|
||||
Ok(Value::Nil)
|
||||
}
|
||||
}
|
||||
})?;
|
||||
// .context("创建包装版 require 函数失败")?;
|
||||
|
||||
// 3. 覆盖全局 require
|
||||
globals.set("require", wrapped_require)?;
|
||||
// .context("重载 require 函数失败")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+58
-166
@@ -1,13 +1,34 @@
|
||||
use crate::Layout;
|
||||
use crate::error::syntax_error;
|
||||
use crate::utils::normalize_path_for_lua;
|
||||
use anyhow::{Context, Result};
|
||||
use mlua::{FromLua, Lua, StdLib, Table, Value};
|
||||
use crate::{Error, Result};
|
||||
use mlua::{FromLua, Lua, StdLib, Value};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::{env, fs};
|
||||
use tracing::{debug, trace, warn};
|
||||
use tracing::debug;
|
||||
|
||||
const MIRROR_DIR: &str = "__MIRROR_DIR__";
|
||||
pub struct LuaRuntimeBuilder {
|
||||
lua: Result<Lua>,
|
||||
}
|
||||
impl LuaRuntimeBuilder {
|
||||
pub fn with<F>(mut self, extension: F) -> Self
|
||||
where
|
||||
F: FnOnce(&Lua) -> Result<()>,
|
||||
{
|
||||
// 如果前面已有错误,直接短路跳过后续操作
|
||||
if let Ok(ref lua) = self.lua {
|
||||
if let Err(e) = extension(lua) {
|
||||
self.lua = Err(e);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// 收敛构建终点:一次性输出统一的 Result
|
||||
pub fn build(self) -> Result<LuaRuntime> {
|
||||
let lua = self.lua?;
|
||||
Ok(LuaRuntime { lua })
|
||||
}
|
||||
}
|
||||
|
||||
/// 将 Layout 相关的公共上下文组合为一个插件项
|
||||
|
||||
pub struct LuaRuntime {
|
||||
lua: Lua,
|
||||
@@ -15,43 +36,28 @@ pub struct LuaRuntime {
|
||||
|
||||
impl LuaRuntime {
|
||||
/// 初始化限定权限的 Lua 沙箱环境
|
||||
pub fn new(layout: &Layout) -> Result<Self> {
|
||||
pub fn builder() -> LuaRuntimeBuilder {
|
||||
// 只加载安全的标准库,剥离 os / io 等风险模块
|
||||
let lua = Lua::new_with(
|
||||
StdLib::TABLE | StdLib::STRING | StdLib::MATH | StdLib::PACKAGE,
|
||||
mlua::LuaOptions::default(),
|
||||
)
|
||||
.context("初始化 Lua 失败")?;
|
||||
.map_err(Error::LuaInit);
|
||||
|
||||
// 统一使用 POSIX 风格路径规范化路径字符串
|
||||
let base_dir = normalize_path_for_lua(&layout.base_dir);
|
||||
// let tools_dir = normalize_path_for_lua(&layout.tools_dir);
|
||||
|
||||
// 1. 注入锚点变量 __MIRROR_DIR__(shim 安装根目录)
|
||||
Self::register_mirror_dir(&lua, &base_dir)?;
|
||||
|
||||
// 2. 安全暴露 get_env 供配置读取环境变量
|
||||
// 返回按平台路径分隔符拆分后的段数组(自动剥离引号包裹),
|
||||
// 便于 PATH 等列表变量直接嵌入数组:PATH = { prefix, get_env("PATH") }
|
||||
Self::register_get_env(&lua)?;
|
||||
// 3. 初始化并配置安全/容错的 require 机制
|
||||
Self::setup_require(&lua, &base_dir)?;
|
||||
|
||||
Ok(Self { lua })
|
||||
LuaRuntimeBuilder { lua }
|
||||
}
|
||||
|
||||
/// 执行指定脚本文件,直接返回完整的 Lua Table
|
||||
pub fn eval_script<T: FromLua>(&self, path: impl AsRef<Path>) -> Result<T> {
|
||||
let path = path.as_ref();
|
||||
|
||||
let bytes =
|
||||
fs::read(path).with_context(|| format!("无法读取配置文件: {}", path.display()))?;
|
||||
let bytes = fs::read(path).map_err(|e| Error::ScriptRead {
|
||||
path: path.to_path_buf(),
|
||||
source: e,
|
||||
})?;
|
||||
|
||||
let code = String::from_utf8(bytes).with_context(|| {
|
||||
format!(
|
||||
"{} 不是有效的 UTF-8 文件(请将 Lua 配置文件保存为 UTF-8 编码)",
|
||||
path.display()
|
||||
)
|
||||
let code = String::from_utf8(bytes).map_err(|_| Error::InvalidUtf8 {
|
||||
path: path.to_path_buf(),
|
||||
})?;
|
||||
debug!("code {:?}", code);
|
||||
// 使用 @<path> 格式标记 Chunk 名称,确保 Lua 报错时能精确回溯到对应的源文件名与行号。
|
||||
@@ -61,150 +67,36 @@ impl LuaRuntime {
|
||||
.load(&code)
|
||||
.set_name(&chunk_name)
|
||||
.eval::<T>()
|
||||
.map_err(syntax_error)
|
||||
.with_context(|| format!("执行 {} 配置文件失败: {}", chunk_name,path.display()))
|
||||
}
|
||||
|
||||
/// 传入注册表的 handler key 与命令行原始参数,内部完成调用
|
||||
pub fn call_handler(
|
||||
&self,
|
||||
handler_key: &mlua::RegistryKey,
|
||||
args: Vec<String>,
|
||||
) -> Result<()> {
|
||||
let handler: mlua::Function = self.lua.registry_value(handler_key)?;
|
||||
let lua_args = self.lua.create_sequence_from(args)?;
|
||||
|
||||
if let Err(err) = handler.call::<()>(lua_args) {
|
||||
let msg = err.to_string();
|
||||
// 内部消化类似 argparse 的帮助退出信号
|
||||
if msg.contains("__ARGPARSE_HELP__") {
|
||||
return Ok(());
|
||||
}
|
||||
anyhow::bail!("{msg}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
.map_err(|e| Error::LuaRuntime {
|
||||
chunk: chunk_name,
|
||||
source: e,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn require_and_run(&self, module_name: &str, raw_args: Vec<String>) -> Result<()> {
|
||||
let require: mlua::Function = self.lua.globals().get("require")?;
|
||||
let handler_val: mlua::Value = require.call(module_name)?;
|
||||
let handler_val: Value = require.call(module_name)?;
|
||||
|
||||
let func = match handler_val {
|
||||
mlua::Value::Function(f) => f,
|
||||
other => anyhow::bail!("模块 '{module_name}' 必须返回一个函数,实际为 {}", other.type_name()),
|
||||
Value::Function(f) => f,
|
||||
other => {
|
||||
return Err(Error::InvalidModuleEntry {
|
||||
module_name: module_name.to_string(),
|
||||
actual_type: other.type_name(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let lua_args = self.lua.create_sequence_from(raw_args)?;
|
||||
func.call::<()>(lua_args)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
}
|
||||
impl LuaRuntime {
|
||||
/// 注入全局锚点变量
|
||||
fn register_mirror_dir(lua: &Lua, base_dir: &str) -> Result<()> {
|
||||
lua.globals()
|
||||
.set(MIRROR_DIR, base_dir)
|
||||
.context(format!("设置 {MIRROR_DIR} 环境变量失败"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///注入 get_env 供配置读取环境变量
|
||||
fn register_get_env(lua: &Lua) -> Result<()> {
|
||||
let globals = lua.globals();
|
||||
let get_env = lua
|
||||
.create_function(|lua, key: String| -> mlua::Result<Table> {
|
||||
// 缺失变量视为空字符串,拆分后得到空表(不贡献任何路径段)
|
||||
let value = env::var_os(key).unwrap_or_default();
|
||||
// 空输入返回空表;否则按平台分隔符拆分(split_paths 会剥离引号包裹)
|
||||
if value.is_empty() {
|
||||
return lua.create_table();
|
||||
}
|
||||
let table = lua.create_table()?;
|
||||
for (i, p) in env::split_paths(&value).enumerate() {
|
||||
// 3. 跨平台提取原始字节并转为 LuaString,保证 100% 无损
|
||||
#[cfg(unix)]
|
||||
let lua_str = {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
lua.create_string(p.as_os_str().as_bytes())?
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
let lua_str = {
|
||||
// Windows 路径是 UTF-16,转成字符串或保持其字节表达
|
||||
let s = p.to_string_lossy();
|
||||
lua.create_string(s.as_bytes())?
|
||||
};
|
||||
|
||||
table.set(i + 1, lua_str)?;
|
||||
}
|
||||
|
||||
Ok(table)
|
||||
})
|
||||
.context("注册 get_env 函数失败")?;
|
||||
|
||||
globals
|
||||
.set("get_env", get_env)
|
||||
.context("挂载 get_env 全局函数失败")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 配置 package.path 并包装 require,拦截加载失败以提高容错性
|
||||
fn setup_require(lua: &Lua, base_dir: &str) -> Result<()> {
|
||||
let globals = lua.globals();
|
||||
|
||||
// 1. 安全加固并拓展 package 搜索路径
|
||||
let package = globals.get::<Table>("package")?;
|
||||
|
||||
let _ = package.set("cpath", "");
|
||||
let _ = package.set("loadlib", Value::Nil);
|
||||
|
||||
let commands_dir = format!("{}/commands", base_dir);
|
||||
let vendor_dir = format!("{}/vendor", base_dir);
|
||||
|
||||
let path = package.get::<String>("path")?;
|
||||
let new_path = format!(
|
||||
"{};{}/?.lua;{}/?/init.lua;{}/?.lua;{}/?/init.lua;{}/?.lua;{}/?/init.lua;",
|
||||
path, base_dir, base_dir, commands_dir, commands_dir, vendor_dir, vendor_dir
|
||||
);
|
||||
|
||||
debug!("重新写入的 package 查找路径");
|
||||
package.set("path", new_path)?;
|
||||
|
||||
// 2. 获取原生 require 并通过闭包直接持有(无需向全局表注入备份变量)
|
||||
let original_require: mlua::Function = globals
|
||||
.get("require")
|
||||
.context("获取内置 require 函数失败")?;
|
||||
|
||||
let wrapped_require = lua
|
||||
.create_function(move |_lua, module: String| -> mlua::Result<Value> {
|
||||
match original_require.call::<Value>(module.as_str()) {
|
||||
Ok(value) => Ok(value),
|
||||
Err(e) => {
|
||||
warn!(
|
||||
module = %module,
|
||||
error = %e,
|
||||
"配置模块加载失败,已跳过该条目(可在独立配置文件中定义)"
|
||||
);
|
||||
Ok(Value::Nil)
|
||||
}
|
||||
}
|
||||
})
|
||||
.context("创建包装版 require 函数失败")?;
|
||||
|
||||
// 3. 覆盖全局 require
|
||||
globals
|
||||
.set("require", wrapped_require)
|
||||
.context("重载 require 函数失败")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{Layout, };
|
||||
use crate::Layout;
|
||||
|
||||
fn test_layout() -> Layout {
|
||||
let root = std::env::temp_dir().join("rshim-test-layout");
|
||||
@@ -217,7 +109,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn missing_module_require_returns_nil() {
|
||||
let runtime = LuaRuntime::new(&test_layout()).unwrap();
|
||||
let runtime = LuaRuntime::builder().build().unwrap();
|
||||
let value: Value = runtime
|
||||
.lua
|
||||
.load(r#"return require("rshim_test_no_such_module")"#)
|
||||
@@ -228,7 +120,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn builtin_module_require_still_works() {
|
||||
let runtime = LuaRuntime::new(&test_layout()).unwrap();
|
||||
let runtime = LuaRuntime::builder().build().unwrap();
|
||||
let value: Value = runtime
|
||||
.lua
|
||||
.load(r#"return pcall(require, "string")"#)
|
||||
@@ -239,7 +131,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn get_env_returns_split_table() {
|
||||
let runtime = LuaRuntime::new(&test_layout()).unwrap();
|
||||
let runtime = LuaRuntime::builder().build().unwrap();
|
||||
let value: Value = runtime
|
||||
.lua
|
||||
.load(r#"return get_env("PATH")"#)
|
||||
@@ -266,7 +158,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn get_env_missing_returns_empty_table() {
|
||||
let runtime = LuaRuntime::new(&test_layout()).unwrap();
|
||||
let runtime = LuaRuntime::builder().build().unwrap();
|
||||
let value: Value = runtime
|
||||
.lua
|
||||
.load(r#"return get_env("RSHIM_TEST_NO_SUCH_VAR_12345")"#)
|
||||
@@ -298,7 +190,7 @@ mod tests {
|
||||
// )
|
||||
// .eval()
|
||||
// .unwrap();
|
||||
//
|
||||
//
|
||||
// let path = cfg.env.get("PATH").unwrap().to_str().unwrap();
|
||||
// let prefix = std::env::temp_dir()
|
||||
// .join("rshim-test-layout")
|
||||
@@ -306,7 +198,7 @@ mod tests {
|
||||
// .replace('\\', "/")
|
||||
// + "/tools/numa;";
|
||||
// assert!(path.starts_with(&prefix), "unexpected PATH: {path}");
|
||||
//
|
||||
//
|
||||
// // 宿主 PATH 的段应被附加在配置前缀之后
|
||||
// let host = std::env::var("PATH").unwrap_or_default();
|
||||
// if !host.is_empty() {
|
||||
@@ -320,7 +212,7 @@ mod tests {
|
||||
// "missing host PATH segment: {host_first}"
|
||||
// );
|
||||
// }
|
||||
//
|
||||
//
|
||||
// // 宿主 PATH 里引号包裹的畸形段(如 "D:\\...\\bin;")应被原样保留,
|
||||
// // 而不是让整个配置加载失败
|
||||
// if host.contains('"') {
|
||||
|
||||
Reference in New Issue
Block a user