From 38dabd2ace1a033aea028325a64f40ff62f20403 Mon Sep 17 00:00:00 2001 From: CNWei Date: Thu, 10 Sep 2026 14:33:54 +0800 Subject: [PATCH] =?UTF-8?q?refactor(core,shim):=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E5=9F=BA=E4=BA=8E=20thiserror=20=E7=9A=84=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E4=BD=93=E7=B3=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 anyhow,使用 thiserror 定义强类型 Error 枚举 - 支持 #[from] 自动转换 mlua::Error,关键路径保留源码与上下文 - LuaRuntimeBuilder 采用惰性错误收集,简化链式初始化调用 - MirrorRegistry 实现规范化 FromLuaConversionError 错误转换 --- Cargo.lock | 1 + commands/commands.lua | 18 +-- mirror-cli/Cargo.toml | 2 +- mirror-cli/src/app.rs | 40 ------ mirror-cli/src/cli.rs | 24 ++-- mirror-cli/src/commands/builtin.rs | 70 ++++++--- mirror-cli/src/commands/dynamic.rs | 50 ++++--- mirror-cli/src/error.rs | 14 ++ mirror-cli/src/executor.rs | 64 +++++++++ mirror-cli/src/main.rs | 46 ++++-- mirror-cli/src/plugins.rs | 24 ++++ mirror-cli/src/validators.rs | 2 +- mirror-core/Cargo.toml | 1 + mirror-core/src/error.rs | 68 ++++++++- mirror-core/src/lib.rs | 12 +- mirror-core/src/logger.rs | 128 +++++++++-------- mirror-core/src/plugins.rs | 111 ++++++++++++++ mirror-core/src/runtime.rs | 224 ++++++++--------------------- mirror-shim/src/executor.rs | 86 +++++++++++ mirror-shim/src/main.rs | 71 ++------- mirror-shim/src/mirror.rs | 102 ++++++------- mirror-shim/src/validators.rs | 10 +- 22 files changed, 686 insertions(+), 482 deletions(-) delete mode 100644 mirror-cli/src/app.rs create mode 100644 mirror-cli/src/error.rs create mode 100644 mirror-cli/src/executor.rs create mode 100644 mirror-cli/src/plugins.rs create mode 100644 mirror-core/src/plugins.rs create mode 100644 mirror-shim/src/executor.rs diff --git a/Cargo.lock b/Cargo.lock index 4886b88..6fab4a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -282,6 +282,7 @@ dependencies = [ "dunce", "mlua", "serde", + "thiserror", "tinyjson", "tracing", "tracing-appender", diff --git a/commands/commands.lua b/commands/commands.lua index a32b699..6638cdb 100644 --- a/commands/commands.lua +++ b/commands/commands.lua @@ -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") } } \ No newline at end of file diff --git a/mirror-cli/Cargo.toml b/mirror-cli/Cargo.toml index 521b7dc..ec1b2c3 100644 --- a/mirror-cli/Cargo.toml +++ b/mirror-cli/Cargo.toml @@ -2,7 +2,7 @@ name = "mirror-cli" version = { workspace = true } edition = { workspace = true } - +description = "Mirror CLI Manager" # 显式指定生成的二进制文件名 [[bin]] name = "mr" diff --git a/mirror-cli/src/app.rs b/mirror-cli/src/app.rs deleted file mode 100644 index c4759fb..0000000 --- a/mirror-cli/src/app.rs +++ /dev/null @@ -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(®istry_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(()), - } -} diff --git a/mirror-cli/src/cli.rs b/mirror-cli/src/cli.rs index 60ad989..4583cc9 100644 --- a/mirror-cli/src/cli.rs +++ b/mirror-cli/src/cli.rs @@ -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 的相对路径)"), ) } diff --git a/mirror-cli/src/commands/builtin.rs b/mirror-cli/src/commands/builtin.rs index 172e8bd..0fda373 100644 --- a/mirror-cli/src/commands/builtin.rs +++ b/mirror-cli/src/commands/builtin.rs @@ -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::("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::("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::("level").copied(); + let new_dir = matches.get_one::("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(()) diff --git a/mirror-cli/src/commands/dynamic.rs b/mirror-cli/src/commands/dynamic.rs index 63a0fd5..2071471 100644 --- a/mirror-cli/src/commands/dynamic.rs +++ b/mirror-cli/src/commands/dynamic.rs @@ -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::("handler")?; + let module_val = table.get::("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::("__is_lazy_command").unwrap_or(false) => { - t.get::("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() ))); } diff --git a/mirror-cli/src/error.rs b/mirror-cli/src/error.rs new file mode 100644 index 0000000..98cfbad --- /dev/null +++ b/mirror-cli/src/error.rs @@ -0,0 +1,14 @@ + +pub struct ExecuteError { + pub code: i32, + pub source: anyhow::Error, +} + +impl ExecuteError { + pub fn new(code: i32, err: impl Into) -> Self { + Self { + code, + source: err.into(), + } + } +} diff --git a/mirror-cli/src/executor.rs b/mirror-cli/src/executor.rs new file mode 100644 index 0000000..80bbad6 --- /dev/null +++ b/mirror-cli/src/executor.rs @@ -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(®istry_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(()) +} diff --git a/mirror-cli/src/main.rs b/mirror-cli/src/main.rs index 238a58f..a3d7a07 100644 --- a/mirror-cli/src/main.rs +++ b/mirror-cli/src/main.rs @@ -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(¤t_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 -} \ No newline at end of file + + exit(EXIT_SUCCESS); +} diff --git a/mirror-cli/src/plugins.rs b/mirror-cli/src/plugins.rs new file mode 100644 index 0000000..43222f3 --- /dev/null +++ b/mirror-cli/src/plugins.rs @@ -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(()) +} diff --git a/mirror-cli/src/validators.rs b/mirror-cli/src/validators.rs index 1a1483d..1b66910 100644 --- a/mirror-cli/src/validators.rs +++ b/mirror-cli/src/validators.rs @@ -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 { diff --git a/mirror-core/Cargo.toml b/mirror-core/Cargo.toml index 7404268..1529630 100644 --- a/mirror-core/Cargo.toml +++ b/mirror-core/Cargo.toml @@ -14,3 +14,4 @@ serde = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } tracing-appender = { workspace = true } +thiserror = "2.0.20" diff --git a/mirror-core/src/error.rs b/mirror-core/src/error.rs index c088b96..8664855 100644 --- a/mirror-core/src/error.rs +++ b/mirror-core/src/error.rs @@ -1,8 +1,21 @@ +use std::path::PathBuf; +use thiserror::Error; // 构造一个带上下文的 FromLua 转换错误,便于定位配置问题 /// 配置校验错误 pub fn validation_error(message: impl Into) -> mlua::Error { mlua::Error::RuntimeError(message.into()) } +pub fn conversion_err( + from: &'static str, + to: impl Into, + msg: impl Into, +) -> 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 -} \ No newline at end of file +} + +/// 全局统一的 `Result` 别名,默认错误类型为 [`Error`]。 +pub type Result = std::result::Result; + +#[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), +} diff --git a/mirror-core/src/lib.rs b/mirror-core/src/lib.rs index 53715b3..99d65b8 100644 --- a/mirror-core/src/lib.rs +++ b/mirror-core/src/lib.rs @@ -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; \ No newline at end of file +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}; diff --git a/mirror-core/src/logger.rs b/mirror-core/src/logger.rs index 39329ca..e52a935 100644 --- a/mirror-core/src/logger.rs +++ b/mirror-core/src/logger.rs @@ -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, } -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 { + 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 { @@ -113,16 +137,11 @@ pub fn init_logging_from(layout: &Layout) -> Option { // 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::(&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 { // 基础终端输出层 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, -) -> (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 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); } diff --git a/mirror-core/src/plugins.rs b/mirror-core/src/plugins.rs new file mode 100644 index 0000000..16cf4a4 --- /dev/null +++ b/mirror-core/src/plugins.rs @@ -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 { + // 缺失变量视为空字符串,拆分后得到空表(不贡献任何路径段) + 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::
("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::("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 { + match original_require.call::(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(()) +} diff --git a/mirror-core/src/runtime.rs b/mirror-core/src/runtime.rs index 7b260f8..d0f1d1a 100644 --- a/mirror-core/src/runtime.rs +++ b/mirror-core/src/runtime.rs @@ -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, +} +impl LuaRuntimeBuilder { + pub fn with(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 { + 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 { + 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(&self, path: impl AsRef) -> Result { 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); // 使用 @ 格式标记 Chunk 名称,确保 Lua 报错时能精确回溯到对应的源文件名与行号。 @@ -61,150 +67,36 @@ impl LuaRuntime { .load(&code) .set_name(&chunk_name) .eval::() - .map_err(syntax_error) - .with_context(|| format!("执行 {} 配置文件失败: {}", chunk_name,path.display())) - } - - /// 传入注册表的 handler key 与命令行原始参数,内部完成调用 - pub fn call_handler( - &self, - handler_key: &mlua::RegistryKey, - args: Vec, - ) -> 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) -> 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
{ - // 缺失变量视为空字符串,拆分后得到空表(不贡献任何路径段) - 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::
("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::("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 { - match original_require.call::(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('"') { diff --git a/mirror-shim/src/executor.rs b/mirror-shim/src/executor.rs new file mode 100644 index 0000000..4eff325 --- /dev/null +++ b/mirror-shim/src/executor.rs @@ -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 = 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 + } + } +} diff --git a/mirror-shim/src/main.rs b/mirror-shim/src/main.rs index dbcb4bf..913fc9a 100644 --- a/mirror-shim/src/main.rs +++ b/mirror-shim/src/main.rs @@ -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(¤t_exe){ + + let layout = match Layout::from(¤t_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 = 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); } diff --git a/mirror-shim/src/mirror.rs b/mirror-shim/src/mirror.rs index cefafce..f88a58a 100644 --- a/mirror-shim/src/mirror.rs +++ b/mirror-shim/src/mirror.rs @@ -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, +pub struct MirrorRegistry { + pub targets: HashMap, } -impl FromLua for MasterMirror { +impl FromLua for MirrorRegistry { fn from_lua(value: Value, lua: &Lua) -> mlua::Result { 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::() { - 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, + /// 参数别名映射表(用于展开以 mr: 为前缀的别名) pub aliases: HashMap>, + /// 注入/覆盖的环境变量 pub env: HashMap, } -impl Mirror { - pub fn load(layout: &Layout) -> Result { - // 策略 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::>(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(&self, args: I) -> Vec 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 { // 脚本返回必须是一个 Table 变体 let table = match value { diff --git a/mirror-shim/src/validators.rs b/mirror-shim/src/validators.rs index 8e6148c..b484fb2 100644 --- a/mirror-shim/src/validators.rs +++ b/mirror-shim/src/validators.rs @@ -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 { + fn parse(src: &str) -> mlua::Result { 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::()?; - let t = Mirror::from_lua(value, &lua); + let t = MirrorTarget::from_lua(value, &lua); println!("读取出的数据:{:?}", t.clone()?); t }