diff --git a/Cargo.lock b/Cargo.lock index e8f1b39..db0f27c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -162,6 +162,7 @@ dependencies = [ "anyhow", "dunce", "mlua", + "serde", "tinyjson", "tracing", "tracing-appender", @@ -325,6 +326,16 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + [[package]] name = "serde_core" version = "1.0.229" diff --git a/Cargo.toml b/Cargo.toml index 0b20fef..5f321df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,9 +30,12 @@ dunce = "1.0.5" tracing = "0.1.44" tracing-subscriber = { version = "0.3", features = ["env-filter","fmt"] } tracing-appender = "0.2" +serde = { version = "1.0.229", features = ["derive"] } tinyjson="2.5.1" +#toml = "1.1.4+spec-1.1.0" + [features] -default = ["args"] +default = [] args=[] \ No newline at end of file diff --git a/mirror.ini b/mirror.ini new file mode 100644 index 0000000..6de024a --- /dev/null +++ b/mirror.ini @@ -0,0 +1,8 @@ +# mirror-log.ini +level = debug +log_dir = "logs" + +# 或者带节结构: +# [logger] +# level = info +# log_dir = /var/log/mirror \ No newline at end of file diff --git a/mirror.lua b/mirror.lua index 48e1537..563be88 100644 --- a/mirror.lua +++ b/mirror.lua @@ -1,6 +1,6 @@ -- mirror.lua (总控制台) -- __MIRROR_DIR__ 已经在 Rust 中注入完毕,并且是正斜杠格式 (例如 C:/Tools) -local base_dir = __MIRROR_DIR__ +base_dir = __MIRROR_DIR__ -- 1. 自定义局部变量,方便复用与后续维护 local python_home = base_dir .. "/tools/python39" diff --git a/src/layout.rs b/src/layout.rs index 7b669b9..bb8879d 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -5,49 +5,53 @@ use tracing::debug; pub struct Layout { pub base_dir: PathBuf, pub bin_dir: PathBuf, - pub tools_dir: PathBuf, - pub lua_file: PathBuf, + pub target_name: String, } impl Layout { /// 自动解析目录布局: /// 1. 优先使用环境变量 MIRROR_HOME /// 2. 兜底回退到当前垫片可执行文件所在目录推断 (exe -> bin -> root) - pub fn discover(current_exe: &Path) -> Result { + pub fn from(current_exe: &Path) -> Result { + let target_name = current_exe + .file_stem() + .and_then(|s| s.to_str()) + .with_context(|| format!("无法从路径 [{}] 提取有效的程序名称", current_exe.display()))? + .to_lowercase(); + // 策略 1: 环境变量优先 if let Ok(home_val) = env::var("MIRROR_HOME") { let trimmed = home_val.trim(); if !trimmed.is_empty() { + debug!(home = %trimmed, "检测到 MIRROR_HOME,采用环境变量配置"); - return Self::from_base_dir(PathBuf::from(trimmed)); + + return Self::from_base_dir(PathBuf::from(trimmed), target_name); } } // 策略 2: 相对路径自动推断兜底 debug!("未配置 MIRROR_HOME,尝试从当前可执行文件路径推断根目录"); - Self::from_executable(current_exe) + Self::from_executable(current_exe, target_name) } /// 基于确定的根目录构建完整布局 - fn from_base_dir(base_dir: PathBuf) -> Result { + fn from_base_dir(base_dir: PathBuf, target_name: String) -> Result { if !base_dir.is_dir() { bail!("指定的根目录不存在或不是有效目录: [{}]", base_dir.display()); } let bin_dir = base_dir.join("bin"); - let tools_dir = base_dir.join("tools"); - let lua_file = base_dir.join("../mirror.lua"); Ok(Self { base_dir, bin_dir, - tools_dir, - lua_file, + target_name, }) } /// 从当前可执行文件解析 shim 安装目录布局 - fn from_executable(exe_path: &Path) -> Result { + fn from_executable(exe_path: &Path, target_name: String) -> Result { // let exe_path = exe_path.as_ref(); let bin_dir = exe_path .parent() @@ -60,8 +64,9 @@ impl Layout { .parent() .with_context(|| format!("无法获取 bin 目录 [{}] 的父级 root 目录", bin_dir.display()))? .to_path_buf(); + debug!("base_dir 目录 {}", base_dir.display()); - Self::from_base_dir(base_dir) + Self::from_base_dir(base_dir, target_name) } } diff --git a/src/lib.rs b/src/lib.rs index 0c7d29e..66a95d3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,8 @@ -extern crate core; - pub mod error; mod layout; -mod loader; mod logger; mod mirror; mod runtime; -mod spec; pub mod sys; mod utils; mod validators; @@ -14,10 +10,6 @@ mod validators; pub use layout::Layout; pub use mirror::Mirror; pub use runtime::LuaRuntime; -pub use spec::MirrorSpec; -// pub use sys::{ -// ERROR_ELEVATION_REQUIRED, EXIT_FAILED_LOAD_SHIM, EXIT_FAILED_SPAWN_PROG, EXIT_FAILED_WAIT_PROG, -// EXIT_PROG_TERMINATED, execute_elevated, set_console_ctrl_handler, -// }; +pub use logger::{Logger, init_logging_from}; pub use utils::{lua_string_2_os_string, normalize_path_for_lua, parse_tokens}; diff --git a/src/loader.rs b/src/loader.rs deleted file mode 100644 index a7a46b5..0000000 --- a/src/loader.rs +++ /dev/null @@ -1,99 +0,0 @@ -use crate::spec::MirrorSpec; -use crate::{Layout, LuaRuntime}; -use anyhow::{Context, Result, bail}; -use mlua::Table; -use std::path::PathBuf; -use tracing::{debug, trace, warn}; - -pub enum Source { - Lua(LuaRuntime), - Json, -} - -/// MirrorSpec 加载器,统一对外暴露多源解析接口 -pub struct SpecLoader; - -impl SpecLoader { - pub fn resolve_spec(source: &Source, layout: &Layout, target_name: &str) -> Result { - match source { - Source::Lua(r) => Self::resolve_lua_spec(&r, layout, target_name), - Source::Json => Self::load_from_json(layout), - } - } - /// 从 Lua 脚本字符串加载 ShimSpec - fn resolve_lua_spec( - runtime: &LuaRuntime, - layout: &Layout, - target_name: &str, - ) -> Result { - // 策略 1: 尝试加载全局配置文件 mirror.lua - let global_config = layout.base_dir.join("mirror.lua"); - - if global_config.is_file() { - trace!(path = %global_config.display(), "发现全局配置文件,尝试解析"); - let root_table: Table = runtime.eval_script(&global_config)?; - - // 检查 mirror.lua 中是否存在以 target_name 命名的 Table 节点 - if root_table - .contains_key(target_name) - .context("检查全局配置失败")? - { - let target_val: MirrorSpec = root_table - .get(target_name) - .with_context(|| format!("解析配置 [{}] 失败: ", target_name))?; - debug!( - target = %target_name, - source = %global_config.display(), - "成功从全局配置文件中匹配到目标工具" - ); - - return Ok(target_val); - } - // 穿透:若全局配置文件存在但未包含当前程序的 key,继续向下探查 - trace!(target = %target_name, "全局配置文件中未包含该目标,继续探查独立配置"); - } - - let tools_dir: Option = runtime.tools_dir()?; - // 策略 2: 降级寻找独立文件 ({exe}.lua),优先顺序:tools/ > root/ - let target_filename = format!("{}.lua", target_name); - - let effective_tools_dir = match tools_dir { - Some(t) => { - if t.is_absolute() { - t.join(&target_filename) - } else { - layout.base_dir.join(t).join(&target_filename) - } - } - None => layout.tools_dir.join(&target_filename), - }; - - let candidates = [ - // layout.tools_dir.join(&target_filename), - effective_tools_dir, - layout.base_dir.join(&target_filename), - ]; - - for config_path in &candidates { - if config_path.is_file() { - debug!(path = %config_path.display(), "找到独立配置文件,开始加载"); - // 直接泛型反序列化为 MirrorSpec - return runtime.eval_script::(config_path); - } - trace!(path = %config_path.display(), "独立配置文件不存在,跳过"); - } - - // 策略 3: 所有查找失败,抛出错误 - warn!(target = %target_name, "未找到任何匹配的配置文件"); - bail!( - "未找到关于 '{}' 的配置。请检查 mirror.lua 或特定的 {}.lua 文件", - target_name, - target_name - ); - } - - /// 从 Lua 脚本文件加载 ShimSpec - fn load_from_json(layout: &Layout) -> Result { - todo!("实现json来源") - } -} diff --git a/src/logger.rs b/src/logger.rs index 3abeac0..0a24ba2 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -1,39 +1,175 @@ -use std::path::Path; -use tracing_appender::non_blocking::WorkerGuard; +use crate::Layout; +use serde::Deserialize; +use std::fs; +use std::path::PathBuf; +use std::str::FromStr; +use tracing_appender::non_blocking::{NonBlocking, WorkerGuard}; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::{EnvFilter, fmt}; +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Deserialize, Default)] +#[serde(rename_all = "lowercase")] +pub enum LogLevel { + Off, + Error, + #[default] + Warn, + Info, + Debug, + Trace, +} +impl LogLevel { + /// 是否需要激活写盘排查模式(Info 及以上更详细的级别) + pub fn is_verbose(&self) -> bool { + *self >= LogLevel::Info + } -/// 初始化日志系统,返回的 `_guard` 必须在 main 作用域内保持存活直到程序退出 -pub fn init_file_logger(log_dir: impl AsRef) -> Option { - // 允许通过环境变量动态控制日志级别,如 SHIM_LOG=debug,默认 debug 或 info - let filter = EnvFilter::try_from_env("SHIM_LOG").unwrap_or_else(|_| EnvFilter::new("debug")); + pub fn as_str(&self) -> &'static str { + match self { + LogLevel::Off => "off", + LogLevel::Error => "error", + LogLevel::Warn => "warn", + LogLevel::Info => "info", + LogLevel::Debug => "debug", + LogLevel::Trace => "trace", + } + } +} +impl FromStr for LogLevel { + type Err = (); - // 1. 创建按天滚动的日志追加器 (每天生成类似 shim.2026-08-14.log) - let file_appender = tracing_appender::rolling::daily(log_dir, "shim.log"); - - // 2. 包装为非阻塞后台写入(不会拖慢主程序的启动与执行速度) - let (non_blocking, guard) = tracing_appender::non_blocking(file_appender); - - // 3. 构建 Subscriber,只输出到文件,不输出到控制台 - tracing_subscriber::fmt() - .with_env_filter(filter) - .with_writer(non_blocking) // 写入文件 - .with_ansi(false) // 关闭终端彩色转义字符 - .with_target(false) // 隐藏模块前缀(可选) - .init(); - - Some(guard) + fn from_str(s: &str) -> Result { + match s.to_ascii_lowercase().as_str() { + "off" => Ok(LogLevel::Off), + "error" => Ok(LogLevel::Error), + "warn" => Ok(LogLevel::Warn), + "info" => Ok(LogLevel::Info), + "debug" => Ok(LogLevel::Debug), + "trace" => Ok(LogLevel::Trace), + _ => Err(()), + } + } } -// 调用 -// fn main() -> Result<(), Box> { -// // 假设日志存放在安装根目录下的 logs 文件夹 -// // 也可以先快速推导 layout 拿到 log_dir -// let log_dir = "path/to/root_dir/logs"; -// let _guard = init_file_logger(log_dir); -// -// // 此处写你的 Shim 业务逻辑 -// // 业务代码中所有的 debug!/info!/warn! 都会静默写入文件,控制台干干净净 -// let config = Shim::load()?; -// -// Ok(()) -// } +#[derive(Debug, Deserialize, Default)] +pub struct Logger { + #[serde(default)] + pub level: LogLevel, + pub log_dir: Option, +} +impl Logger { + /// 零依赖解析 ini / key-value 文本内容 + pub fn parse_ini(content: &str) -> Self { + let mut logger = 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('[') + { + continue; + } + + if let Some((key, val)) = line.split_once('=') { + let key = key.trim(); + let val = val.trim().trim_matches('"').trim_matches('\''); + + match key { + "level" => { + if let Ok(lvl) = LogLevel::from_str(val) { + logger.level = lvl; + } + } + "log_dir" => { + if !val.is_empty() { + logger.log_dir = Some(PathBuf::from(val)); + } + } + _ => {} + } + } + } + + logger + } +} +/// 基于 Layout 自动寻址 mirror.toml 并初始化日志系统。 +/// 仅在开启详细日志(如 info/debug/trace)时激活异步文件记录并返回 `WorkerGuard`。 +pub fn init_logging_from(layout: &Layout) -> Option { + // 1. 在 layout.base_dir 目录下寻找 log.toml + // 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. 优先读取环境变量 SHIM_LOG,解析失败或未设置则退回到 config.level + let effective_level = std::env::var("MIRROR_LOG_LEVEL") + .ok() + .and_then(|val| LogLevel::from_str(&val).ok()) + .unwrap_or(config.level); + if effective_level == LogLevel::Off { + return None; + } + let filter = EnvFilter::new(effective_level.as_str()); + // 基础终端输出层 + 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() + .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) + } 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"), + }; + + if let Err(err) = fs::create_dir_all(&output_dir) { + eprintln!("[WARN] 创建日志目录 {} 失败: {}", output_dir.display(), err); + } + + let file_appender = tracing_appender::rolling::daily(output_dir, "mirror.log"); + tracing_appender::non_blocking(file_appender) +} diff --git a/src/main.rs b/src/main.rs index 0869dc1..b25f371 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,29 +1,50 @@ -use mirror::Mirror; +use mirror::{Layout, Mirror,init_logging_from}; use mirror::sys::*; use std::ffi::OsString; use std::{env, process::exit}; -use tracing_subscriber::{EnvFilter, fmt}; +use anyhow::Context; +use tracing::{debug, error, warn}; fn main() { - //初始化日志:输出到 stderr,避免污染 shim 子进程的 stdout - fmt() - .with_writer(std::io::stderr) - .with_env_filter( - EnvFilter::try_from_env("SHIM_LOG").unwrap_or_else(|_| EnvFilter::new("warn")), - ) - .init(); - // 2. 注册 Windows 控制台信号 - set_console_ctrl_handler(); - // 3. 解析调用参数与代理 Mirror 配置 - let calling_args: Vec<_> = env::args_os().skip(1).collect(); - let mr = match Mirror::new() { - Ok(v) => v, + let current_exe = match env::current_exe().context("获取代理程序路径失败"){ + Ok(exe) => exe, Err(e) => { - eprintln!("加载代理(mirror)配置时发生错误: {}", e); + eprintln!("垫片启动失败: {:#}", e); exit(EXIT_FAILED_LOAD_SHIM); } }; - let combined_args = mr.spec.args.iter().chain(calling_args.iter()); + let layout =match Layout::from(¤t_exe){ + Ok(l) => l, + Err(e) => { + eprintln!("解析程序目录布局失败: {:#}", e); + exit(EXIT_FAILED_LOAD_SHIM); + } + }; + + + //初始化日志:输出到 stderr,避免污染 shim 子进程的 stdout + let _guard = init_logging_from(&layout); + + debug!("=== 垫片程序启动 ==="); + debug!( + exe = %current_exe.display(), + target_name = %layout.target_name, + "日志系统与布局上下文已就位" + ); + + // 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); @@ -35,15 +56,15 @@ fn main() { let elevated_args: Vec = cmd.get_args().map(|s| s.to_os_string()).collect(); exit(execute_elevated( - &mr.spec.target, + &mr.target, &elevated_args, - Some(&mr.spec.env), + Some(&mr.env), )) } Err(e) => { - eprintln!( + error!( "启动目标程序 [{}] 时发生错误: {}", - mr.spec.target.to_string_lossy(), + mr.target.to_string_lossy(), e ); exit(EXIT_FAILED_SPAWN_PROG); @@ -53,9 +74,9 @@ fn main() { let status = match child.wait() { Ok(v) => v, Err(e) => { - eprintln!( + error!( "等待目标程序 [{}] 执行完毕时发生错误: {}", - mr.spec.target.to_string_lossy(), + mr.target.to_string_lossy(), e ); exit(EXIT_FAILED_WAIT_PROG); diff --git a/src/mirror.rs b/src/mirror.rs index b2f9c48..bc57114 100644 --- a/src/mirror.rs +++ b/src/mirror.rs @@ -1,54 +1,58 @@ -use crate::loader::Source; -use crate::loader::SpecLoader; -use crate::{Layout, LuaRuntime, MirrorSpec}; -use anyhow::{Context, Result}; +use crate::error::validation_error; +use crate::validators::LuaValidator; +use crate::{Layout, LuaRuntime}; +use anyhow::{Context, Result, bail}; +use mlua::{FromLua, Lua, Table, Value}; use std::collections::HashMap; use std::env; use std::ffi::OsString; +use std::path::PathBuf; use std::process::Command; -use tracing::debug; +use tracing::{debug, trace}; +#[derive(Debug, Clone, Default)] pub struct Mirror { - pub spec: MirrorSpec, - pub layout: Layout, - pub target_name: String, + pub target: PathBuf, + pub args: Vec, + pub aliases: HashMap>, + pub env: HashMap, } impl Mirror { - pub fn new() -> Result { - let current_exe = env::current_exe().context("获取代理程序路径失败: {}")?; - debug!("当前目录 {}", current_exe.display()); + pub fn load(layout: &Layout) -> Result { + // 策略 1: 尝试加载全局配置文件 mirror.lua + let global_config = layout.base_dir.join("mirror.lua"); - let target_name = current_exe - .file_stem() - .and_then(|s| s.to_str()) - .with_context(|| format!("无法从路径 [{}] 提取有效的程序名称", current_exe.display()))? - .to_lowercase(); - debug!( - target_name = %target_name, - current_exe = %current_exe.display(), - "开始加载 Mirror 配置" - ); - let layout = Layout::discover(¤t_exe)?; + 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!( - root_dir = %layout.base_dir.display(), - bin_dir = %layout.bin_dir.display(), - tools_dir = %layout.tools_dir.display(), - "Mirror 目录布局解析完成" + target = %target_name, + source = %global_config.display(), + "成功从全局配置文件中匹配到目标工具" ); - let runtime = LuaRuntime::new(&layout)?; - let spec = SpecLoader::resolve_spec(&Source::Lua(runtime), &layout, &target_name)?; - - Ok(Self { - spec, - layout, - target_name, - }) + Ok(target_val) } /// 1. 核心参数路由解析(Delegation 到 Spec 的路由逻辑) - fn resolve_args(&self, args: I, aliases: &HashMap>) -> Vec + fn resolve_args(&self, args: I) -> Vec where I: IntoIterator, S: AsRef, @@ -67,7 +71,7 @@ impl Mirror { // 检查参数是否带有 mr: 前缀 if let Some(alias_key) = arg_str.strip_prefix("mr:") { // 如果在加载期打平好的字典中找到了对应的别名,直接展开追加 - if let Some(alias_values) = aliases.get(alias_key) { + if let Some(alias_values) = self.aliases.get(alias_key) { expanded_args.extend(alias_values.iter().cloned()); } else { // 如果找不到对应的别名,按原样参数追加 @@ -86,16 +90,16 @@ impl Mirror { I: IntoIterator, S: AsRef, { - let mut cmd = Command::new(&self.spec.target); + let mut cmd = Command::new(&self.target); // 2. 传入预打平的别名字典,查表并展开所有以 `mr:` 为前缀的别名 - let final_args = self.resolve_args(combined_args, &self.spec.aliases); - println!("拼接后的命令行参数{:?}",final_args); + let final_args = self.resolve_args(combined_args); + debug!("拼接后的命令行参数{:?}", final_args); // 3. 将解析展开后的无环参数一次性注入 Command cmd.args(&final_args); // 4. 注入配置好的环境变量 - for (key, val) in &self.spec.env { + for (key, val) in &self.env { // 直接应用环境变量(Lua 端已经处理好字符串拼接或列表合并) cmd.env(key, val); } @@ -103,3 +107,50 @@ impl Mirror { cmd } } +impl FromLua for Mirror { + fn from_lua(value: Value, _lua: &Lua) -> mlua::Result { + // 脚本返回必须是一个 Table 变体 + let table = match value { + Value::Table(t) => t, + _ => { + return Err(validation_error(format!( + "期望得到一个 Lua Table 配置对象,实际是 {}", + value.type_name() + ))); + } + }; + + // 必填字段: target(严格限定为字符串,避免数字被 mlua 宽松转为字符串后掩盖错误) + let target = match table.get::>("target")? { + None | Some(Value::Nil) => { + return Err(validation_error("缺少必填字段 target(应为字符串路径)")); + } + Some(target_val) => LuaValidator::parse_target(&target_val)?, + }; + + // 可选字段: args(缺失或 nil 时默认空列表;保留空字符串参数,与提权路径的 "" 语义一致) + let args = match table.get::>("args")? { + None | Some(Value::Nil) => Vec::new(), + Some(args_val) => LuaValidator::parse_args(&args_val)?, + }; + + // 可选字段: env(只允许缺失/nil,其他类型由 Option 转换报错,不再静默忽略) + let env = match table.get::>("env")? { + None | Some(Value::Nil) => HashMap::new(), + Some(env_val) => LuaValidator::parse_env(&env_val)?, + }; + + // 可选字段: aliases(只允许缺失/nil,其他类型由 Option
转换报错,不再静默忽略) + let aliases = match table.get::>("aliases")? { + None | Some(Value::Nil) => HashMap::new(), + Some(env_val) => LuaValidator::parse_aliases(&env_val)?, + }; + // println!("环境变量结果:{:?}", env); + Ok(Self { + target, + args, + aliases, + env, + }) + } +} diff --git a/src/runtime.rs b/src/runtime.rs index 0ddf74b..6f37f59 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -3,13 +3,11 @@ use crate::error::syntax_error; use crate::utils::normalize_path_for_lua; use anyhow::{Context, Result}; use mlua::{FromLua, Lua, StdLib, Table, Value}; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::{env, fs}; +use tracing::{debug, trace, warn}; const MIRROR_DIR: &str = "__MIRROR_DIR__"; -const MIRROR_TOOLS_DIR: &str = "__MIRROR_TOOLS_DIR__"; -// const MIRROR_LOG_LEVEL: &str = "__MIRROR_LOG_LEVEL__"; -const MIRROR_LOG_DIR: &str = "__MIRROR_LOG_DIR__"; pub struct LuaRuntime { lua: Lua, @@ -27,7 +25,7 @@ impl LuaRuntime { // 统一使用 POSIX 风格路径规范化路径字符串 let base_dir = normalize_path_for_lua(&layout.base_dir); - let tools_dir = normalize_path_for_lua(&layout.tools_dir); + // let tools_dir = normalize_path_for_lua(&layout.tools_dir); // 1. 注入锚点变量 __MIRROR_DIR__(shim 安装根目录) Self::register_mirror_dir(&lua, &base_dir)?; @@ -37,7 +35,7 @@ impl LuaRuntime { // 便于 PATH 等列表变量直接嵌入数组:PATH = { prefix, get_env("PATH") } Self::register_get_env(&lua)?; // 3. 初始化并配置安全/容错的 require 机制 - Self::setup_require(&lua, &base_dir, &tools_dir)?; + Self::setup_require(&lua, &base_dir)?; Ok(Self { lua }) } @@ -54,7 +52,7 @@ impl LuaRuntime { path.display() ) })?; - tracing::trace!("code {:?}", code); + debug!("code {:?}", code); // 使用 @ 格式标记 Chunk 名称,确保 Lua 报错时能精确回溯到对应的源文件名与行号。 let chunk_name = format!("@{}", path.display()); @@ -65,13 +63,6 @@ impl LuaRuntime { .map_err(syntax_error) .with_context(|| format!("执行 Lua 配置文件失败: {}", path.display())) } - - pub fn tools_dir(&self) -> Result> { - self.lua - .globals() - .get(MIRROR_TOOLS_DIR) - .context(format!("设置 {MIRROR_TOOLS_DIR} 失败")) - } } impl LuaRuntime { /// 注入全局锚点变量 @@ -122,33 +113,8 @@ impl LuaRuntime { Ok(()) } - /// 通用的路径注册闭包生成器 - fn register_dir_reset_fn( - lua: &Lua, - global_fn_name: &str, - target_global_key: &'static str, - ) -> Result<()> { - let get_env = lua.create_function(move |lua, rel_path: String| { - let clean_path = rel_path.trim().trim_start_matches('/'); - - lua.globals().set(target_global_key, clean_path)?; - - Ok(()) - })?; - - // 4. 将函数绑定至 Lua 全局作用域,供 Lua 调用 - lua.globals().set(global_fn_name, get_env)?; - Ok(()) - } - - fn register_reset_tools_dir(lua: &Lua) -> Result<()> { - Self::register_dir_reset_fn(lua, "reset_tools_dir", MIRROR_TOOLS_DIR) - } - fn register_reset_log_dir(lua: &Lua) -> Result<()> { - Self::register_dir_reset_fn(lua, "reset_log_dir", MIRROR_LOG_DIR) - } /// 配置 package.path 并包装 require,拦截加载失败以提高容错性 - fn setup_require(lua: &Lua, base_dir: &str, tools_dir: &str) -> Result<()> { + fn setup_require(lua: &Lua, base_dir: &str) -> Result<()> { let globals = lua.globals(); // 1. 安全加固并拓展 package 搜索路径 @@ -157,10 +123,8 @@ impl LuaRuntime { let _ = package.set("loadlib", Value::Nil); if let Ok(path) = package.get::("path") { - let new_path = format!( - "{};{}/?.lua;{}/?/init.lua;{}/?.lua;{}/?/init.lua", - path, base_dir, base_dir, tools_dir, tools_dir - ); + let new_path = format!("{};{}/?.lua;{}/?/init.lua;", path, base_dir, base_dir); + debug!("重新写入的 package 查找路径"); let _ = package.set("path", new_path); } } @@ -175,7 +139,7 @@ impl LuaRuntime { match original_require.call::(module.as_str()) { Ok(value) => Ok(value), Err(e) => { - tracing::warn!( + warn!( module = %module, error = %e, "配置模块加载失败,已跳过该条目(可在独立配置文件中定义)" @@ -197,15 +161,14 @@ impl LuaRuntime { #[cfg(test)] mod tests { use super::*; - use crate::{Layout, MirrorSpec}; + use crate::{Layout, Mirror}; fn test_layout() -> Layout { let root = std::env::temp_dir().join("rshim-test-layout"); Layout { base_dir: root.clone(), bin_dir: root.join("bin"), - tools_dir: root.join("tools"), - lua_file: Default::default(), + target_name: "muna".to_string(), } } @@ -277,7 +240,7 @@ mod tests { // 复现用户场景:PATH = { base_dir .. "/tools/numa", get_env("PATH") } // 宿主 PATH 即使含双引号,也应拆分后正常拼接,而不是报错 let runtime = LuaRuntime::new(&test_layout()).unwrap(); - let cfg: MirrorSpec = runtime + let cfg: Mirror = runtime .lua .load( r#" diff --git a/src/spec.rs b/src/spec.rs deleted file mode 100644 index c71bdf2..0000000 --- a/src/spec.rs +++ /dev/null @@ -1,109 +0,0 @@ -use crate::error::validation_error; -use crate::validators::{JsonValidator, LuaValidator}; -use anyhow::{Result, anyhow}; -use mlua::{FromLua, Lua, Value}; -use std::collections::HashMap; -use std::ffi::OsString; -use std::path::PathBuf; -use std::str::FromStr; -use tinyjson::JsonValue; - - -#[derive(Debug, Clone, Default)] -pub struct MirrorSpec { - pub target: PathBuf, - pub args: Vec, - pub aliases: HashMap>, - pub env: HashMap, -} - -/// 实现 FromLua Trait,由 mlua 自动处理 Table 转换 -impl FromLua for MirrorSpec { - fn from_lua(value: Value, _lua: &Lua) -> mlua::Result { - // 脚本返回必须是一个 Table 变体 - let table = match value { - Value::Table(t) => t, - _ => { - return Err(validation_error(format!( - "期望得到一个 Lua Table 配置对象,实际是 {}", - value.type_name() - ))); - } - }; - - // 必填字段: target(严格限定为字符串,避免数字被 mlua 宽松转为字符串后掩盖错误) - let target = match table.get::>("target")? { - None | Some(Value::Nil) => { - return Err(validation_error("缺少必填字段 target(应为字符串路径)")); - } - Some(target_val) => LuaValidator::parse_target(&target_val)?, - }; - - // 可选字段: args(缺失或 nil 时默认空列表;保留空字符串参数,与提权路径的 "" 语义一致) - let args = match table.get::>("args")? { - None | Some(Value::Nil) => Vec::new(), - Some(args_val) => LuaValidator::parse_args(&args_val)?, - }; - - // 可选字段: env(只允许缺失/nil,其他类型由 Option
转换报错,不再静默忽略) - let env = match table.get::>("env")? { - None | Some(Value::Nil) => HashMap::new(), - Some(env_val) => LuaValidator::parse_env(&env_val)?, - }; - - // 可选字段: aliases(只允许缺失/nil,其他类型由 Option
转换报错,不再静默忽略) - let aliases = match table.get::>("aliases")? { - None | Some(Value::Nil) => HashMap::new(), - Some(env_val) => LuaValidator::parse_aliases(&env_val)?, - }; - // println!("环境变量结果:{:?}", env); - Ok(MirrorSpec { - target, - args, - aliases, - env, - }) - } -} - -impl TryFrom<&str> for MirrorSpec { - type Error = anyhow::Error; - - fn try_from(json_str: &str) -> Result { - // 1. 解析 JSON 字符串为 JsonValue 树 - let root = JsonValue::from_str(json_str).map_err(|e| anyhow!("JSON 语法错误: {}", e))?; - - // 根节点必须是一个 JSON Object - let map: &HashMap = root - .get() - .ok_or_else(|| anyhow!("JSON 根节点必须是 Object 对象"))?; - - // 1. target (必填) - let target_val = map - .get("target") - .ok_or_else(|| anyhow!("缺少必填字段: target"))?; - let target = JsonValidator::parse_target(target_val)?; - - // 2. args (选填) - let args = match map.get("args") { - Some(args_val) => JsonValidator::parse_args(args_val)?, - None => Vec::new(), - }; - - // 3. env (选填) - let env = match map.get("env") { - Some(env_val) => JsonValidator::parse_env(env_val)?, - None => HashMap::new(), - }; - let aliases = match map.get("aliases") { - Some(env_val) => JsonValidator::parse_aliases(env_val)?, - None => HashMap::new(), - }; - Ok(MirrorSpec { - target, - args, - aliases, - env, - }) - } -} diff --git a/src/sys/win.rs b/src/sys/win.rs index b2d4b2d..9bbed77 100644 --- a/src/sys/win.rs +++ b/src/sys/win.rs @@ -2,7 +2,7 @@ use std::os::windows::ffi::OsStrExt; use std::{env, mem::size_of, path::Path, ptr::null_mut}; use std::ffi::{OsStr, OsString}; - +use tracing::error; use windows_sys::Win32::UI::Shell::{SHELLEXECUTEINFOW, ShellExecuteExW}; use windows_sys::Win32::Foundation::CloseHandle; @@ -40,7 +40,7 @@ unsafe extern "system" fn console_ctrl_handler(evt: u32) -> BOOL { CTRL_LOGOFF_EVENT => TRUE, //eprintln!("ctrl_logoff handled!"), CTRL_SHUTDOWN_EVENT => TRUE, //eprintln!("ctrl_shutdown handled!"), other => { - eprintln!("未知的系统事件编号: {},未处理!", other); + error!("未知的系统事件编号: {},未处理!", other); FALSE } } @@ -49,7 +49,7 @@ unsafe extern "system" fn console_ctrl_handler(evt: u32) -> BOOL { pub fn set_console_ctrl_handler() { let res: BOOL = unsafe { SetConsoleCtrlHandler(Some(console_ctrl_handler), TRUE) }; if res == FALSE { - eprintln!("警告: 注册控制台中断事件处理器失败。"); + error!("警告: 注册控制台中断事件处理器失败。"); } } diff --git a/src/validators.rs b/src/validators.rs index ff68f1a..d8a176c 100644 --- a/src/validators.rs +++ b/src/validators.rs @@ -8,6 +8,7 @@ use std::fmt; use std::path::PathBuf; use std::str::FromStr; use tinyjson::JsonValue; +use tracing::debug; /// Lua 值校验器:针对不同上下文定义校验规则 #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -231,13 +232,13 @@ impl LuaValidator { /// 解析并校验 `args` pub fn parse_args(value: &Value) -> mlua::Result> { { - println!("跟踪args1{}", value.type_name()); + debug!("args 配置已关闭 "); #[cfg(not(feature = "args"))] Ok(Vec::new()) } #[cfg(feature = "args")] { - println!("跟踪args1{}", value.type_name()); + debug!("args 配置已开启"); let ctx = Self::Args; match value { Value::Table(tbl) => { @@ -246,7 +247,7 @@ impl LuaValidator { let capacity = tbl.raw_len().min(128); let mut raw_parts = Vec::with_capacity(capacity); - + Self::collect_value_into(&ctx, value, &mut raw_parts)?; for part in &raw_parts { @@ -319,7 +320,7 @@ impl LuaValidator { n += 1; let (raw_key, raw_val) = pair?; - println!("env {} {}", n, raw_val.type_name()); + debug!("env {} {}", n, raw_val.type_name()); // 1. 解析并校验 Key,拿到安全的 String let key = Self::parse_env_name(&raw_key)?; @@ -395,7 +396,7 @@ impl LuaValidator { // 阶段三:无环前提下的高效展开 let mut flattened_aliases: HashMap> = HashMap::with_capacity(raw_aliases.len()); - + let mut resolved_args = Vec::new(); for key in raw_aliases.keys() { @@ -467,118 +468,13 @@ impl LuaValidator { } } -/// 专用于 JSON (tinyjson) 的类型校验与字段提取器 -//当前JsonValidator还为完全可用,后期会对其lua配置特别需要处理动态函数生成的值如:get_env("PATH") -// 数据结构:{ -// "target": "C:/tools/git.exe", -// "args": [ -// "--no-pager", -// 2, -// true -// ], -// "env": { -// "PATH": ["C:/tools/git/bin", "C:/Windows"], -// "HOME": "C:/tools/home", -// "CONST": 3, -// "BOOL": true -// } -// } -pub struct JsonValidator; - -impl JsonValidator { - /// 校验并解析 target (必填,非空字符串) - pub fn parse_target(val: &JsonValue) -> Result { - let s: &String = val - .get() - .ok_or_else(|| anyhow!("字段 'target' 必须是字符串"))?; - - if s.trim().is_empty() { - return Err(anyhow!("字段 'target' 不能为空字符串")); - } - Ok(PathBuf::from(s)) - } - - /// 校验并解析 args (选填,必须为字符串数组) - pub fn parse_args(val: &JsonValue) -> Result> { - let args_vec: &Vec = - val.get().ok_or_else(|| anyhow!("字段 'args' 必须是数组"))?; - - args_vec - .iter() - .map(|item| { - let s: &String = item - .get() - .ok_or_else(|| anyhow!("'args' 数组内的元素必须全为字符串"))?; - Ok(OsString::from(s)) - }) - .collect() - } - - /// 校验并解析 env (选填,必须为 KV 对象) - pub fn parse_env(val: &JsonValue) -> Result> { - let env_map: &HashMap = val - .get() - .ok_or_else(|| anyhow!("字段 'env' 必须是 Object 键值对"))?; - let mut result = HashMap::new(); - for (k, v) in env_map { - let mut segments = Vec::new(); - Self::collect_env_segments(v, &mut segments)?; - - if !segments.is_empty() { - // 根据操作系统自动拼接环境变量分隔符 (Windows 为 ';', Unix 为 ':') - #[cfg(windows)] - let sep = std::ffi::OsStr::new(";"); - #[cfg(not(windows))] - let sep = std::ffi::OsStr::new(":"); - - let joined = segments.join(sep); - result.insert(k.clone(), joined); - } - } - - Ok(result) - } - pub fn parse_aliases(val: &JsonValue) -> Result>> { - todo!() - } - // ========================================================================= - // 私有辅助方法 - // ========================================================================= - - /// 单个标量节点转换为 OsString (支持 String, Number, Boolean) - fn value_to_os_string(val: &JsonValue, ctx: &str) -> Result { - if let Some(s) = val.get::() { - Ok(OsString::from(s)) - } else if let Some(n) = val.get::() { - Ok(OsString::from(n.to_string())) - } else if let Some(b) = val.get::() { - Ok(OsString::from(b.to_string())) - } else { - Err(anyhow!("{} 内只支持字符串、数字或布尔类型", ctx)) - } - } - - /// 递归打平 env 节点中的数组/标量值 - fn collect_env_segments(val: &JsonValue, segments: &mut Vec) -> Result<()> { - if let Some(arr) = val.get::>() { - for item in arr { - Self::collect_env_segments(item, segments)?; - } - } else { - let parsed_segment = Self::value_to_os_string(val, "'env' 节点的 Value")?; - segments.push(parsed_segment); - } - Ok(()) - } -} - #[cfg(test)] mod tests { use super::*; - use crate::MirrorSpec; + use crate::Mirror; 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 @@ -597,7 +493,7 @@ mod tests { lua.globals().set("get_env", get_env).unwrap(); let value = lua.load(src).eval::()?; - let t = MirrorSpec::from_lua(value, &lua); + let t = Mirror::from_lua(value, &lua); println!("读取出的数据:{:?}", t.clone()?); t }