refactor(core,shim): 统一基于 thiserror 的错误体系

- 移除 anyhow,使用 thiserror 定义强类型 Error 枚举
- 支持 #[from] 自动转换 mlua::Error,关键路径保留源码与上下文
- LuaRuntimeBuilder 采用惰性错误收集,简化链式初始化调用
- MirrorRegistry 实现规范化 FromLuaConversionError 错误转换
This commit is contained in:
2026-09-10 14:33:54 +08:00
parent 94b4c23299
commit 38dabd2ace
22 changed files with 686 additions and 482 deletions
Generated
+1
View File
@@ -282,6 +282,7 @@ dependencies = [
"dunce",
"mlua",
"serde",
"thiserror",
"tinyjson",
"tracing",
"tracing-appender",
+4 -14
View File
@@ -1,37 +1,27 @@
-- commands/commands.lua
function command(mod_path)
-- 无论输入 "a/b/c" 还是 "a\b\c",统一替换为标准的 "a.b.c"
local normalized = mod_path:gsub("[/\\]", ".")
return {
__is_lazy_command = true,
module = normalized
}
end
return {
-- 1. 单文件子命令 (commands/use.lua)
["use"] = {
description = "Switch the active version of a tool",
handler = command("use")
module = mr.command("use")
},
-- 2. 单文件子命令 (commands/list.lua)
["list"] = {
description = "List available and currently activated versions",
handler = command("list")
module = mr.command("list")
},
-- 3. 复杂多文件子命令 (加载 commands/log/init.lua)
["log"] = {
description = "View or configure mirror runtime logs",
handler = command("log")
module = mr.command("log")
},
-- 4. 支持别名快捷映射 (例如 mr ls 等价于 mr list)
["ls"] = {
description = "Alias for list",
handler = command("list")
module = mr.command("list")
}
}
+1 -1
View File
@@ -2,7 +2,7 @@
name = "mirror-cli"
version = { workspace = true }
edition = { workspace = true }
description = "Mirror CLI Manager"
# 显式指定生成的二进制文件名
[[bin]]
name = "mr"
-40
View File
@@ -1,40 +0,0 @@
use crate::cli;
use crate::commands::{builtin, dynamic::MrCommand};
use anyhow::{Context, Result, bail};
use tracing::debug;
use mirror_core::{init_logging_from, Layout, LuaRuntime};
pub fn run() -> Result<()> {
let exe = std::env::current_exe().context("获取代理程序路径失败")?;
let layout = Layout::from(&exe)?;
let _guard = init_logging_from(&layout);
debug!("=== 管理程序启动 ===");
let registry_file = layout.base_dir.join("commands").join("commands.lua");
let runtime = LuaRuntime::new(&layout)?;
let commands: MrCommand = runtime
.eval_script(&registry_file)
.with_context(|| format!("加载命令配置失败: {}", registry_file.display()))?;
let app = cli::build(commands.clone());
let matches = app.get_matches();
match matches.subcommand() {
// 分支 A: 原生 Rust 实现的命令
Some(("log", sub_m)) => builtin::log_handle(&layout, sub_m),
// 分支 B: 动态 Lua 注册的命令
Some((cmd_name, sub_m)) => {
let sub_cmd = commands
.get(cmd_name)
.with_context(|| format!("未找到子命令 '{cmd_name}'"))?;
let raw_args = cli::extract_raw_args(sub_m);
runtime.require_and_run(&sub_cmd.module, raw_args)
}
// 分支 C: 未输入子命令(通常已被 Clap 的 arg_required_else_help 拦截)
None => Ok(()),
}
}
+8 -16
View File
@@ -5,8 +5,8 @@ use std::ffi::OsString;
// 引入强类型枚举
pub fn build(dynamic_cmds: MrCommand) -> Command {
let mut app = Command::new("mr")
.about("Mirror CLI Manager")
.version("0.1.0")
.about(env!("CARGO_PKG_DESCRIPTION"))
.version(env!("CARGO_PKG_VERSION"))
.arg_required_else_help(true)
.subcommand(log_subcommand());
@@ -28,28 +28,20 @@ pub fn build(dynamic_cmds: MrCommand) -> Command {
fn log_subcommand() -> Command {
Command::new("log")
.about("查看或修改 mirror.ini 日志配置")
.arg(
Arg::new("tail")
.short('t')
.long("tail")
.value_name("LINES")
.help("显示最近的 N 行日志")
.value_parser(clap::value_parser!(usize)),
)
.arg(
Arg::new("level")
.short('l')
.long("level")
.value_name("LEVEL")
.help("设置运行时日志级别")
.help("设置日志级别 (off, error, warn, info, debug, trace)")
.value_parser(clap::value_parser!(LogLevel)),
)
.arg(
Arg::new("follow")
.short('f')
.long("follow")
.help("持续跟踪日志输出 (tail -f)")
.action(ArgAction::SetTrue),
Arg::new("dir")
.short('d')
.long("dir")
.value_name("PATH")
.help("设置日志存放路径(绝对路径或相对于 base_dir 的相对路径)"),
)
}
+47 -23
View File
@@ -1,34 +1,58 @@
use anyhow::{Context, Result};
use mirror_core::{Layout, LogLevel};
use mirror_core::{Layout, LogConfig, LogLevel};
use std::fs;
use std::path::PathBuf;
// Rust 原生处理 log 命令逻辑
pub(crate) fn log_handle(layout: &Layout, matches: &clap::ArgMatches) -> Result<()> {
let log_ini_path = layout.base_dir.join("../../../mirror-core/mirror.ini");
let log_ini_path = layout.base_dir.join("mirror.ini");
// 1. 处理设置日志等级
if let Some(&level) = matches.get_one::<LogLevel>("level") {
let content = format!("level = \"{}\"\nlog_dir = \"logs\"\n", level.as_str());
fs::write(&log_ini_path, content)
.with_context(|| format!("写入日志配置文件失败: {}", log_ini_path.display()))?;
println!("日志级别已更新为: {level} ({})", log_ini_path.display());
return Ok(());
}
// 2. 处理 tail 查看
if let Some(&tail_lines) = matches.get_one::<usize>("tail") {
println!("正在检索最后 {tail_lines} 行日志...");
// 原生 Rust 高性能按行倒序读取日志文件
return Ok(());
}
// 3. 默认打印当前配置
if log_ini_path.is_file() {
let current_ini = fs::read_to_string(&log_ini_path)
let new_level = matches.get_one::<LogLevel>("level").copied();
let new_dir = matches.get_one::<String>("dir").map(|s| s.trim());
// 1. 反序列化:读取并解析现有配置;若文件不存在则使用默认配置
let mut config: LogConfig = if log_ini_path.is_file() {
let content = fs::read_to_string(&log_ini_path)
.with_context(|| format!("读取配置文件失败: {}", log_ini_path.display()))?;
println!("当前 mirror.ini 配置:\n{current_ini}");
content.parse().unwrap_or_default()
} else {
println!("未找到 mirror.ini,当前使用默认全局级别: warn");
LogConfig::default()
};
// 2. 修改配置并写盘(仅更新用户指定的字段,未指定的字段保留原值)
if new_level.is_some() || new_dir.is_some() {
if let Some(lvl) = new_level {
config.level = lvl;
}
if let Some(dir) = new_dir {
config.log_dir = Some(PathBuf::from(dir));
}
// 序列化写回
fs::write(&log_ini_path, config.to_string())
.with_context(|| format!("写入日志配置文件失败: {}", log_ini_path.display()))?;
let actual_dir = config.resolve_log_dir(&layout.base_dir);
println!("✔ 日志配置已更新:");
println!(" 日志级别: {}", config.level.as_str());
println!(" 日志目录: {}", actual_dir.display());
println!(" 配置文件: {}", log_ini_path.display());
return Ok(());
}
// 3. 无修改参数时,展示当前配置状态
println!("=== Mirror 日志配置 ===");
println!("配置文件: {}", log_ini_path.display());
println!("---------------------");
if log_ini_path.is_file() {
print!("{config}");
} else {
println!("状态: 配置文件尚未创建");
println!("当前生效级别: {} (默认)", config.level.as_str());
println!(
"默认日志目录: {}",
config.resolve_log_dir(&layout.base_dir).display()
);
println!("\n提示: 可使用 'mr log -l debug' 或 'mr log -d my_logs' 进行配置修改");
}
Ok(())
+30 -20
View File
@@ -1,5 +1,5 @@
use crate::validators;
use mirror_core::error::validation_error;
use mirror_core::{conversion_err, validation_error};
use mlua::{FromLua, Lua, Value};
use std::collections::HashMap;
@@ -20,10 +20,14 @@ impl FromLua for MrCommand {
let root_tbl = match value {
Value::Table(t) => t,
other => {
return Err(validation_error(format!(
"commands 注册表的顶层配置必须是 Table,实际检测到: {}",
other.type_name()
)));
return Err(conversion_err(
other.type_name(),
"MrCommand",
format!(
"commands 注册表的顶层配置必须是 Table,实际检测到: {}",
other.type_name()
),
));
}
};
@@ -35,10 +39,14 @@ impl FromLua for MrCommand {
let cmd_name = validators::validate_command_name(&cmd_name)?;
// 2. 校验 entry 是否是 Table
if !cmd_entry.is_table() {
return Err(validation_error(format!(
"子命令 '{cmd_name}' 的配置必须是 Table,实际检测到: {}",
cmd_entry.type_name()
)));
return Err(conversion_err(
cmd_entry.type_name(),
"MrCommand",
format!(
"子命令 '{cmd_name}' 的配置必须是 Table,实际检测到: {}",
cmd_entry.type_name()
),
));
}
// 3. 直接交由子命令解析,外层负责补充错误上下文
let cmd_def = MrSubCommand::from_lua(cmd_entry, lua).map_err(|err| {
@@ -55,7 +63,7 @@ pub struct MrSubCommand {
/// 命令处理函数:必须是 Lua Function
pub(crate) module: String,
/// 命令行说明:必须是普通文本字符串
pub(crate) description: String ,
pub(crate) description: String,
}
impl FromLua for MrSubCommand {
@@ -63,10 +71,11 @@ impl FromLua for MrSubCommand {
let table = match value {
Value::Table(t) => t,
other => {
return Err(validation_error(format!(
"配置必须是 Table,实际检测到: {}",
other.type_name()
)));
return Err(conversion_err(
other.type_name(),
"MrSubCommand",
format!("配置必须是 Table,实际检测到: {}", other.type_name()),
));
}
};
// validators::LuaValidator::parse_command_entry(&tbl)
@@ -83,7 +92,7 @@ impl FromLua for MrSubCommand {
)));
}
}
let handler_val = table.get::<Value>("handler")?;
let module_val = table.get::<Value>("module")?;
// let handler_func = match handler_val {
// Value::Function(f) => f,
// Value::Boolean(true) => {
@@ -105,14 +114,15 @@ impl FromLua for MrSubCommand {
// )));
// }
// };
let module = match handler_val {
Value::Table(t) if t.get::<bool>("__is_lazy_command").unwrap_or(false) => {
t.get::<String>("module")?
let module = match module_val {
Value::String(s) => {
// 兜底归一化(防止用户手写 module = "a/b" 时未走 mr.command)
// 同时兼容直接写字符串的情况
s.to_str()?.replace(['/', '\\'], ".")
}
Value::String(s) => s.to_str()?.to_string(), // 同时兼容直接写字符串的情况
other => {
return Err(mlua::Error::runtime(format!(
"handler 必须通过 command(\"xxx\") 声明,实际为 {}",
"module 必须通过 mr.command(\"xxx\") 声明,实际为 {}",
other.type_name()
)));
}
+14
View File
@@ -0,0 +1,14 @@
pub struct ExecuteError {
pub code: i32,
pub source: anyhow::Error,
}
impl ExecuteError {
pub fn new(code: i32, err: impl Into<anyhow::Error>) -> Self {
Self {
code,
source: err.into(),
}
}
}
+64
View File
@@ -0,0 +1,64 @@
use crate::cli;
use crate::commands::{builtin, dynamic::MrCommand};
use crate::error::ExecuteError;
use crate::plugins::register_mr_command;
use anyhow::{Context, Result};
use mirror_core::{Layout, LuaRuntime, register_basics};
// 成功的标准退出码
pub const EXIT_SUCCESS: i32 = 0;
// mr CLI 专有错误退出码
pub const EXIT_CONFIG_ERROR: i32 = 10; // 配置文件加载、解析失败
pub const EXIT_RUNTIME_ERROR: i32 = 11; // Lua 运行时初始化或脚本执行异常
pub const EXIT_COMMAND_NOT_FOUND: i32 = 12; // 找不到指定的子命令
pub const EXIT_BUILTIN_ERROR: i32 = 13; // 原生命令(如 log)执行失败
pub fn run(layout: &Layout) -> Result<(), ExecuteError> {
let registry_file = layout.base_dir.join("commands").join("commands.lua");
let runtime = LuaRuntime::builder()
.with(|lua| register_basics(lua, layout))
.with(register_mr_command)
.build()
.map_err(|e| ExecuteError::new(EXIT_RUNTIME_ERROR, e))?;
let commands: MrCommand = runtime
.eval_script(&registry_file)
.with_context(|| format!("加载命令配置失败: {}", registry_file.display()))
.map_err(|e| ExecuteError::new(EXIT_CONFIG_ERROR, e))?;
let app = cli::build(commands.clone());
let matches = app.get_matches();
match matches.subcommand() {
// 分支 A: 原生 Rust 实现的命令
Some(("log", sub_m)) => {
builtin::log_handle(&layout, sub_m)
.map_err(|e| ExecuteError::new(EXIT_BUILTIN_ERROR, e))?;
}
// 分支 B: 动态 Lua 注册的命令
Some((cmd_name, sub_m)) => {
let sub_cmd = commands.get(cmd_name).ok_or_else(|| {
ExecuteError::new(
EXIT_COMMAND_NOT_FOUND,
anyhow::anyhow!("未找到子命令 '{cmd_name}'"),
)
})?;
let raw_args = cli::extract_raw_args(sub_m);
runtime
.require_and_run(&sub_cmd.module, raw_args)
.map_err(|e| {
ExecuteError::new(
EXIT_COMMAND_NOT_FOUND,
anyhow::anyhow!("执行子命令 '{}' 失败: {:#}", cmd_name, e),
)
})?;
}
// 分支 C: 未输入子命令(通常已被 Clap 的 arg_required_else_help 拦截)
None => {}
}
Ok(())
}
+38 -8
View File
@@ -1,14 +1,44 @@
mod app;
use crate::executor::EXIT_SUCCESS;
use anyhow::Context;
use mirror_core::{Layout, init_logging_from};
use std::process::exit;
use tracing::{debug, error};
mod cli;
mod commands;
mod error;
mod executor;
mod plugins;
mod validators;
use std::process::ExitCode;
fn main() {
let current_exe = match std::env::current_exe().context("获取代理程序路径失败") {
Ok(exe) => exe,
Err(e) => {
eprintln!("垫片启动失败: {:#}", e);
exit(1);
}
};
let layout = match Layout::from(&current_exe) {
Ok(l) => l,
Err(e) => {
eprintln!("解析程序目录布局失败: {:#}", e);
exit(1);
}
};
let _guard = init_logging_from(&layout);
fn main() -> ExitCode {
if let Err(err) = app::run() {
eprintln!("\x1b[31m[mr error]\x1b[0m {:#}", err);
return ExitCode::FAILURE; // 返回退出码 1
debug!("=== 管理程序启动 ===");
debug!(
exe = %current_exe.display(),
target_name = %layout.target_name,
"日志系统与布局上下文已就位"
);
// 核心运行:成功退出 0,失败根据 AppError 里的 code 退出
if let Err(app_err) = executor::run(&layout) {
error!("{:#}", app_err.source);
exit(app_err.code);
}
ExitCode::SUCCESS // 返回退出码 0
}
exit(EXIT_SUCCESS);
}
+24
View File
@@ -0,0 +1,24 @@
use mirror_core::{Error, Result};
use mlua::{Lua, Table, Value};
/// 仅存在于 CLI 侧的 DSL 扩展函数
pub fn register_mr_command(lua: &Lua) -> Result<()> {
let globals = lua.globals();
let mr_table: Table = match globals.get("mr")? {
Value::Table(tbl) => tbl,
_ => {
let tbl = lua.create_table()?;
globals.set("mr", &tbl)?;
tbl
}
};
let command_fn = lua.create_function(|_lua, mod_path: String| {
let normalized = mod_path.replace(['/', '\\'], ".");
Ok(normalized)
})?;
mr_table.set("command", command_fn)?;
Ok(())
}
+1 -1
View File
@@ -1,4 +1,4 @@
use mirror_core::error::validation_error;
use mirror_core::validation_error;
use mlua::Value;
pub fn validate_command_name(name: &Value) -> mlua::Result<String> {
+1
View File
@@ -14,3 +14,4 @@ serde = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
tracing-appender = { workspace = true }
thiserror = "2.0.20"
+67 -1
View File
@@ -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>),
}
+8 -4
View File
@@ -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
View File
@@ -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);
}
+111
View File
@@ -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
View File
@@ -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('"') {
+86
View File
@@ -0,0 +1,86 @@
use crate::mirror::MirrorRegistry;
use crate::sys::{
ERROR_ELEVATION_REQUIRED, EXIT_FAILED_LOAD_SHIM, EXIT_FAILED_SPAWN_PROG, EXIT_FAILED_WAIT_PROG,
EXIT_PROG_TERMINATED, execute_elevated,
};
use mirror_core::{Layout, LuaRuntime, register_basics};
use std::env;
use std::ffi::OsString;
use std::process::exit;
use tracing::{error, warn};
pub fn run(layout: &Layout) -> i32 {
let global_config = &layout.base_dir.join("mirror.lua");
if !global_config.is_file() {
error!("主配置文件不存在: {}", global_config.display());
return EXIT_FAILED_LOAD_SHIM;
}
let runtime = match LuaRuntime::builder()
.with(|lua| register_basics(lua, &layout))
.build()
{
Ok(r) => r,
Err(e) => {
error!("初始化 Lua 失败: {:#}", e);
return EXIT_FAILED_LOAD_SHIM;
}
};
let registry: MirrorRegistry = match runtime.eval_script(&global_config) {
Ok(t) => t,
Err(e) => {
error!("加载命令配置 [{}] 失败: {:#}", global_config.display(), e);
return EXIT_FAILED_LOAD_SHIM;
}
};
let target_name = layout.target_name.as_str();
let mr = match registry.targets.get(target_name) {
Some(m) => m,
None => {
warn!("未在 mirror.lua 中找到目标配置 [{}]", target_name);
return EXIT_FAILED_LOAD_SHIM;
}
};
// 3. 解析调用参数与代理 Mirror 配置
let calling_args: Vec<_> = env::args_os().skip(1).collect();
let combined_args = mr.args.iter().chain(calling_args.iter());
// 构建 Command:复用 ShimConfig::to_command(含 args/env 注入),避免重复逻辑
let mut cmd = mr.to_command(combined_args);
let mut child = match cmd.spawn() {
Ok(v) => v,
Err(e) if e.raw_os_error() == Some(ERROR_ELEVATION_REQUIRED) => {
// 提权回退时需要完整参数:配置默认参数 + 调用方透传参数
let elevated_args: Vec<OsString> = cmd.get_args().map(|s| s.to_os_string()).collect();
exit(execute_elevated(&mr.target, &elevated_args, Some(&mr.env)))
}
Err(e) => {
error!(
"启动目标程序 [{}] 时发生错误: {}",
mr.target.to_string_lossy(),
e
);
return EXIT_FAILED_SPAWN_PROG;
}
};
// 5. 等待子进程退出
match child.wait() {
Ok(status) => status.code().unwrap_or(EXIT_PROG_TERMINATED),
Err(e) => {
error!(
"等待目标程序 [{}] 执行完毕时发生错误: {}",
mr.target.to_string_lossy(),
e
);
EXIT_FAILED_WAIT_PROG
}
}
}
+12 -59
View File
@@ -1,25 +1,25 @@
use mirror_core::{init_logging_from, Layout, };
use crate::mirror::Mirror;
use std::ffi::OsString;
use std::{env, process::exit};
use anyhow::Context;
use tracing::{debug, error};
use crate::sys::*;
use anyhow::Context;
use mirror_core::{Layout, init_logging_from};
use std::{env, process::exit};
use tracing::debug;
pub mod sys;
mod executor;
pub mod mirror;
pub mod validators;
pub mod sys;
pub mod utils;
pub mod validators;
fn main() {
let current_exe = match env::current_exe().context("获取代理程序路径失败"){
let current_exe = match env::current_exe().context("获取代理程序路径失败") {
Ok(exe) => exe,
Err(e) => {
eprintln!("垫片启动失败: {:#}", e);
exit(EXIT_FAILED_LOAD_SHIM);
}
};
let layout =match Layout::from(&current_exe){
let layout = match Layout::from(&current_exe) {
Ok(l) => l,
Err(e) => {
eprintln!("解析程序目录布局失败: {:#}", e);
@@ -27,7 +27,6 @@ fn main() {
}
};
//初始化日志:输出到 stderr,避免污染 shim 子进程的 stdout
let _guard = init_logging_from(&layout);
@@ -41,52 +40,6 @@ fn main() {
// 2. 注册 Windows 控制台信号
set_console_ctrl_handler();
// 3. 解析调用参数与代理 Mirror 配置
let calling_args: Vec<_> = env::args_os().skip(1).collect();
let mr = match Mirror::load(&layout) {
Ok(v) => v,
Err(e) => {
error!("加载代理(mirror)配置时发生错误: {}", e);
exit(EXIT_FAILED_LOAD_SHIM);
}
};
let combined_args = mr.args.iter().chain(calling_args.iter());
// 构建 Command:复用 ShimConfig::to_command(含 args/env 注入),避免重复逻辑
let mut cmd = mr.to_command(combined_args);
let mut child = match cmd.spawn() {
Ok(v) => v,
Err(e) if e.raw_os_error() == Some(ERROR_ELEVATION_REQUIRED) => {
// 提权回退时需要完整参数:配置默认参数 + 调用方透传参数
let elevated_args: Vec<OsString> = cmd.get_args().map(|s| s.to_os_string()).collect();
exit(execute_elevated(
&mr.target,
&elevated_args,
Some(&mr.env),
))
}
Err(e) => {
error!(
"启动目标程序 [{}] 时发生错误: {}",
mr.target.to_string_lossy(),
e
);
exit(EXIT_FAILED_SPAWN_PROG);
}
};
// 5. 等待子进程退出
let status = match child.wait() {
Ok(v) => v,
Err(e) => {
error!(
"等待目标程序 [{}] 执行完毕时发生错误: {}",
mr.target.to_string_lossy(),
e
);
exit(EXIT_FAILED_WAIT_PROG);
}
};
exit(status.code().unwrap_or(EXIT_PROG_TERMINATED))
let code = executor::run(&layout);
exit(code);
}
+41 -61
View File
@@ -1,6 +1,8 @@
use mirror_core::error::validation_error;
use crate::validators::LuaValidator;
use anyhow::{Context, Result, bail};
use mirror_core::validate_command_name;
use mirror_core::{Layout, LuaRuntime};
use mirror_core::{conversion_err, validation_error};
use mlua::{FromLua, Lua, Table, Value};
use std::collections::HashMap;
use std::env;
@@ -8,89 +10,66 @@ use std::ffi::OsString;
use std::path::PathBuf;
use std::process::Command;
use tracing::{debug, trace};
use mirror_core::{Layout, LuaRuntime};
use mirror_core::validate_command_name;
struct MasterMirror{
master:HashMap<String,Mirror>,
pub struct MirrorRegistry {
pub targets: HashMap<String, MirrorTarget>,
}
impl FromLua for MasterMirror {
impl FromLua for MirrorRegistry {
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> {
let root_tbl = match value {
Value::Table(t) => t,
other => {
return Err(validation_error(format!(
"mirror 注册表的顶层配置必须是 Table,实际检测到: {}",
other.type_name()
)));
return Err(conversion_err(
other.type_name(),
"MirrorRegistry",
format!(
"mirror 全局注册表顶层必须是 Table,实际检测到: {}",
other.type_name()
),
));
}
};
let mut master = HashMap::new();
let mut targets = HashMap::new();
for pair in root_tbl.pairs::<Value, Value>() {
let (cmd_name, cmd_entry) = pair?;
let (key, entry) = pair?;
let cmd_name = validate_command_name(&cmd_name)?;
let target_name = validate_command_name(&key)?;
// 2. 校验 entry 是否是 Table
if !cmd_entry.is_table() {
return Err(validation_error(format!(
"子命令 '{cmd_name}' 的配置必须是 Table,实际检测到: {}",
cmd_entry.type_name()
)));
if !entry.is_table() {
return Err(conversion_err(
entry.type_name(),
"MirrorRegistry",
format!(
"目标工具 '{target_name}' 的配置必须是 Table,实际检测到: {}",
entry.type_name()
),
));
}
// 3. 直接交由子命令解析,外层负责补充错误上下文
let cmd_def = Mirror::from_lua(cmd_entry, lua).map_err(|err| {
validation_error(format!("子命令 '{cmd_name}' 配置解析失败:\n{err}"))
let target_spec = MirrorTarget::from_lua(entry, lua).map_err(|err| {
validation_error(format!("目标工具 '{target_name}' 配置解析失败:\n{err}"))
})?;
master.insert(cmd_name, cmd_def);
targets.insert(target_name, target_spec);
}
Ok(Self { master })
}}
Ok(Self { targets })
}
}
/// 单个目标程序的代理规格配置
#[derive(Debug, Clone, Default)]
pub struct Mirror {
pub struct MirrorTarget {
/// 目标程序可执行文件路径
pub target: PathBuf,
/// 默认预设参数列表
pub args: Vec<OsString>,
/// 参数别名映射表(用于展开以 mr: 为前缀的别名)
pub aliases: HashMap<String, Vec<OsString>>,
/// 注入/覆盖的环境变量
pub env: HashMap<String, OsString>,
}
impl Mirror {
pub fn load(layout: &Layout) -> Result<Self> {
// 策略 1: 尝试加载全局配置文件 mirror.lua
let global_config = layout.base_dir.join("mirror.lua");
if !global_config.is_file() {
bail!("主配置文件不存在: {}", global_config.display());
}
let runtime = LuaRuntime::new(layout)?;
let root_table: Table = runtime.eval_script(&global_config)?;
let target_name = layout.target_name.as_str();
// 检查 mirror.lua 中是否存在以 target_name 命名的 Table 节点
let target_val: Mirror = root_table
.get::<Option<Mirror>>(target_name)
.with_context(|| format!("读取目标配置 [{}] 失败", target_name))?
.ok_or_else(|| {
anyhow::anyhow!(
"在 'mirror.lua' 中未找到目标程序 [{}] 的配置注册",
target_name
)
})?;
debug!(
target = %target_name,
source = %global_config.display(),
"成功从全局配置文件中匹配到目标工具"
);
Ok(target_val)
}
impl MirrorTarget {
/// 1. 核心参数路由解析(Delegation 到 Spec 的路由逻辑)
fn resolve_args<I, S>(&self, args: I) -> Vec<OsString>
where
@@ -147,7 +126,8 @@ impl Mirror {
cmd
}
}
impl FromLua for Mirror {
impl FromLua for MirrorTarget {
fn from_lua(value: Value, _lua: &Lua) -> mlua::Result<Self> {
// 脚本返回必须是一个 Table 变体
let table = match value {
+5 -5
View File
@@ -1,5 +1,5 @@
use mirror_core::error::validation_error;
use anyhow::{Context, Result, anyhow};
use mirror_core::validation_error;
use mlua::{LuaString, Table, Value};
use std::collections::{HashMap, HashSet};
use std::ffi::OsString;
@@ -7,8 +7,8 @@ use std::fmt;
use std::path::PathBuf;
use std::str::FromStr;
// use tinyjson::JsonValue;
use tracing::debug;
use crate::utils::{lua_string_2_os_string, parse_tokens};
use tracing::debug;
/// Lua 值校验器:针对不同上下文定义校验规则
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -471,10 +471,10 @@ impl LuaValidator {
#[cfg(test)]
mod tests {
use super::*;
use crate::Mirror;
use crate::mirror::MirrorTarget;
use mlua::{FromLua, Lua};
fn parse(src: &str) -> mlua::Result<Mirror> {
fn parse(src: &str) -> mlua::Result<MirrorTarget> {
let lua = Lua::new();
// 模拟 runtime 注入的 get_env:返回按平台分隔符拆分的段数组(空变量返回空表)
let get_env = lua
@@ -493,7 +493,7 @@ mod tests {
lua.globals().set("get_env", get_env).unwrap();
let value = lua.load(src).eval::<Value>()?;
let t = Mirror::from_lua(value, &lua);
let t = MirrorTarget::from_lua(value, &lua);
println!("读取出的数据:{:?}", t.clone()?);
t
}