Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bba69fd51 | ||
|
|
38dabd2ace |
Generated
+1
@@ -282,6 +282,7 @@ dependencies = [
|
|||||||
"dunce",
|
"dunce",
|
||||||
"mlua",
|
"mlua",
|
||||||
"serde",
|
"serde",
|
||||||
|
"thiserror",
|
||||||
"tinyjson",
|
"tinyjson",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-appender",
|
"tracing-appender",
|
||||||
|
|||||||
+4
-14
@@ -1,37 +1,27 @@
|
|||||||
-- commands/commands.lua
|
-- 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 {
|
return {
|
||||||
-- 1. 单文件子命令 (commands/use.lua)
|
-- 1. 单文件子命令 (commands/use.lua)
|
||||||
["use"] = {
|
["use"] = {
|
||||||
description = "Switch the active version of a tool",
|
description = "Switch the active version of a tool",
|
||||||
handler = command("use")
|
module = mr.command("use")
|
||||||
},
|
},
|
||||||
|
|
||||||
-- 2. 单文件子命令 (commands/list.lua)
|
-- 2. 单文件子命令 (commands/list.lua)
|
||||||
["list"] = {
|
["list"] = {
|
||||||
description = "List available and currently activated versions",
|
description = "List available and currently activated versions",
|
||||||
handler = command("list")
|
module = mr.command("list")
|
||||||
},
|
},
|
||||||
|
|
||||||
-- 3. 复杂多文件子命令 (加载 commands/log/init.lua)
|
-- 3. 复杂多文件子命令 (加载 commands/log/init.lua)
|
||||||
["log"] = {
|
["log"] = {
|
||||||
description = "View or configure mirror runtime logs",
|
description = "View or configure mirror runtime logs",
|
||||||
handler = command("log")
|
module = mr.command("log")
|
||||||
},
|
},
|
||||||
|
|
||||||
-- 4. 支持别名快捷映射 (例如 mr ls 等价于 mr list)
|
-- 4. 支持别名快捷映射 (例如 mr ls 等价于 mr list)
|
||||||
["ls"] = {
|
["ls"] = {
|
||||||
description = "Alias for list",
|
description = "Alias for list",
|
||||||
handler = command("list")
|
module = mr.command("list")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
name = "mirror-cli"
|
name = "mirror-cli"
|
||||||
version = { workspace = true }
|
version = { workspace = true }
|
||||||
edition = { workspace = true }
|
edition = { workspace = true }
|
||||||
|
description = "Mirror CLI Manager"
|
||||||
# 显式指定生成的二进制文件名
|
# 显式指定生成的二进制文件名
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "mr"
|
name = "mr"
|
||||||
|
|||||||
@@ -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(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+8
-16
@@ -5,8 +5,8 @@ use std::ffi::OsString;
|
|||||||
// 引入强类型枚举
|
// 引入强类型枚举
|
||||||
pub fn build(dynamic_cmds: MrCommand) -> Command {
|
pub fn build(dynamic_cmds: MrCommand) -> Command {
|
||||||
let mut app = Command::new("mr")
|
let mut app = Command::new("mr")
|
||||||
.about("Mirror CLI Manager")
|
.about(env!("CARGO_PKG_DESCRIPTION"))
|
||||||
.version("0.1.0")
|
.version(env!("CARGO_PKG_VERSION"))
|
||||||
.arg_required_else_help(true)
|
.arg_required_else_help(true)
|
||||||
.subcommand(log_subcommand());
|
.subcommand(log_subcommand());
|
||||||
|
|
||||||
@@ -28,28 +28,20 @@ pub fn build(dynamic_cmds: MrCommand) -> Command {
|
|||||||
fn log_subcommand() -> Command {
|
fn log_subcommand() -> Command {
|
||||||
Command::new("log")
|
Command::new("log")
|
||||||
.about("查看或修改 mirror.ini 日志配置")
|
.about("查看或修改 mirror.ini 日志配置")
|
||||||
.arg(
|
|
||||||
Arg::new("tail")
|
|
||||||
.short('t')
|
|
||||||
.long("tail")
|
|
||||||
.value_name("LINES")
|
|
||||||
.help("显示最近的 N 行日志")
|
|
||||||
.value_parser(clap::value_parser!(usize)),
|
|
||||||
)
|
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("level")
|
Arg::new("level")
|
||||||
.short('l')
|
.short('l')
|
||||||
.long("level")
|
.long("level")
|
||||||
.value_name("LEVEL")
|
.value_name("LEVEL")
|
||||||
.help("设置运行时日志级别")
|
.help("设置日志级别 (off, error, warn, info, debug, trace)")
|
||||||
.value_parser(clap::value_parser!(LogLevel)),
|
.value_parser(clap::value_parser!(LogLevel)),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("follow")
|
Arg::new("dir")
|
||||||
.short('f')
|
.short('d')
|
||||||
.long("follow")
|
.long("dir")
|
||||||
.help("持续跟踪日志输出 (tail -f)")
|
.value_name("PATH")
|
||||||
.action(ArgAction::SetTrue),
|
.help("设置日志存放路径(绝对路径或相对于 base_dir 的相对路径)"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,34 +1,58 @@
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use mirror_core::{Layout, LogLevel};
|
use mirror_core::{Layout, LogConfig, LogLevel};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
// Rust 原生处理 log 命令逻辑
|
// Rust 原生处理 log 命令逻辑
|
||||||
pub(crate) fn log_handle(layout: &Layout, matches: &clap::ArgMatches) -> Result<()> {
|
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. 处理设置日志等级
|
let new_level = matches.get_one::<LogLevel>("level").copied();
|
||||||
if let Some(&level) = matches.get_one::<LogLevel>("level") {
|
let new_dir = matches.get_one::<String>("dir").map(|s| s.trim());
|
||||||
let content = format!("level = \"{}\"\nlog_dir = \"logs\"\n", level.as_str());
|
// 1. 反序列化:读取并解析现有配置;若文件不存在则使用默认配置
|
||||||
fs::write(&log_ini_path, content)
|
let mut config: LogConfig = if log_ini_path.is_file() {
|
||||||
.with_context(|| format!("写入日志配置文件失败: {}", log_ini_path.display()))?;
|
let content = fs::read_to_string(&log_ini_path)
|
||||||
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)
|
|
||||||
.with_context(|| format!("读取配置文件失败: {}", log_ini_path.display()))?;
|
.with_context(|| format!("读取配置文件失败: {}", log_ini_path.display()))?;
|
||||||
println!("当前 mirror.ini 配置:\n{current_ini}");
|
content.parse().unwrap_or_default()
|
||||||
} else {
|
} 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(())
|
Ok(())
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::validators;
|
use crate::validators;
|
||||||
use mirror_core::error::validation_error;
|
use mirror_core::{conversion_err, validation_error};
|
||||||
use mlua::{FromLua, Lua, Value};
|
use mlua::{FromLua, Lua, Value};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
@@ -20,10 +20,14 @@ impl FromLua for MrCommand {
|
|||||||
let root_tbl = match value {
|
let root_tbl = match value {
|
||||||
Value::Table(t) => t,
|
Value::Table(t) => t,
|
||||||
other => {
|
other => {
|
||||||
return Err(validation_error(format!(
|
return Err(conversion_err(
|
||||||
|
other.type_name(),
|
||||||
|
"MrCommand",
|
||||||
|
format!(
|
||||||
"commands 注册表的顶层配置必须是 Table,实际检测到: {}",
|
"commands 注册表的顶层配置必须是 Table,实际检测到: {}",
|
||||||
other.type_name()
|
other.type_name()
|
||||||
)));
|
),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -35,10 +39,14 @@ impl FromLua for MrCommand {
|
|||||||
let cmd_name = validators::validate_command_name(&cmd_name)?;
|
let cmd_name = validators::validate_command_name(&cmd_name)?;
|
||||||
// 2. 校验 entry 是否是 Table
|
// 2. 校验 entry 是否是 Table
|
||||||
if !cmd_entry.is_table() {
|
if !cmd_entry.is_table() {
|
||||||
return Err(validation_error(format!(
|
return Err(conversion_err(
|
||||||
|
cmd_entry.type_name(),
|
||||||
|
"MrCommand",
|
||||||
|
format!(
|
||||||
"子命令 '{cmd_name}' 的配置必须是 Table,实际检测到: {}",
|
"子命令 '{cmd_name}' 的配置必须是 Table,实际检测到: {}",
|
||||||
cmd_entry.type_name()
|
cmd_entry.type_name()
|
||||||
)));
|
),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
// 3. 直接交由子命令解析,外层负责补充错误上下文
|
// 3. 直接交由子命令解析,外层负责补充错误上下文
|
||||||
let cmd_def = MrSubCommand::from_lua(cmd_entry, lua).map_err(|err| {
|
let cmd_def = MrSubCommand::from_lua(cmd_entry, lua).map_err(|err| {
|
||||||
@@ -63,10 +71,11 @@ impl FromLua for MrSubCommand {
|
|||||||
let table = match value {
|
let table = match value {
|
||||||
Value::Table(t) => t,
|
Value::Table(t) => t,
|
||||||
other => {
|
other => {
|
||||||
return Err(validation_error(format!(
|
return Err(conversion_err(
|
||||||
"配置必须是 Table,实际检测到: {}",
|
other.type_name(),
|
||||||
other.type_name()
|
"MrSubCommand",
|
||||||
)));
|
format!("配置必须是 Table,实际检测到: {}", other.type_name()),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// validators::LuaValidator::parse_command_entry(&tbl)
|
// 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 {
|
// let handler_func = match handler_val {
|
||||||
// Value::Function(f) => f,
|
// Value::Function(f) => f,
|
||||||
// Value::Boolean(true) => {
|
// Value::Boolean(true) => {
|
||||||
@@ -105,14 +114,15 @@ impl FromLua for MrSubCommand {
|
|||||||
// )));
|
// )));
|
||||||
// }
|
// }
|
||||||
// };
|
// };
|
||||||
let module = match handler_val {
|
let module = match module_val {
|
||||||
Value::Table(t) if t.get::<bool>("__is_lazy_command").unwrap_or(false) => {
|
Value::String(s) => {
|
||||||
t.get::<String>("module")?
|
// 兜底归一化(防止用户手写 module = "a/b" 时未走 mr.command)
|
||||||
|
// 同时兼容直接写字符串的情况
|
||||||
|
s.to_str()?.replace(['/', '\\'], ".")
|
||||||
}
|
}
|
||||||
Value::String(s) => s.to_str()?.to_string(), // 同时兼容直接写字符串的情况
|
|
||||||
other => {
|
other => {
|
||||||
return Err(mlua::Error::runtime(format!(
|
return Err(mlua::Error::runtime(format!(
|
||||||
"handler 必须通过 command(\"xxx\") 声明,实际为 {}",
|
"module 必须通过 mr.command(\"xxx\") 声明,实际为 {}",
|
||||||
other.type_name()
|
other.type_name()
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(())
|
||||||
|
}
|
||||||
+37
-7
@@ -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 cli;
|
||||||
mod commands;
|
mod commands;
|
||||||
|
mod error;
|
||||||
|
mod executor;
|
||||||
|
mod plugins;
|
||||||
mod validators;
|
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 {
|
debug!("=== 管理程序启动 ===");
|
||||||
if let Err(err) = app::run() {
|
debug!(
|
||||||
eprintln!("\x1b[31m[mr error]\x1b[0m {:#}", err);
|
exe = %current_exe.display(),
|
||||||
return ExitCode::FAILURE; // 返回退出码 1
|
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);
|
||||||
}
|
}
|
||||||
@@ -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,4 +1,4 @@
|
|||||||
use mirror_core::error::validation_error;
|
use mirror_core::validation_error;
|
||||||
use mlua::Value;
|
use mlua::Value;
|
||||||
|
|
||||||
pub fn validate_command_name(name: &Value) -> mlua::Result<String> {
|
pub fn validate_command_name(name: &Value) -> mlua::Result<String> {
|
||||||
|
|||||||
@@ -14,3 +14,4 @@ serde = { workspace = true }
|
|||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
tracing-subscriber = { workspace = true }
|
tracing-subscriber = { workspace = true }
|
||||||
tracing-appender = { workspace = true }
|
tracing-appender = { workspace = true }
|
||||||
|
thiserror = "2.0.20"
|
||||||
|
|||||||
@@ -1,8 +1,22 @@
|
|||||||
|
use std::fmt;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use thiserror::Error;
|
||||||
// 构造一个带上下文的 FromLua 转换错误,便于定位配置问题
|
// 构造一个带上下文的 FromLua 转换错误,便于定位配置问题
|
||||||
/// 配置校验错误
|
/// 配置校验错误
|
||||||
pub fn validation_error(message: impl Into<String>) -> mlua::Error {
|
pub fn validation_error(message: impl Into<String>) -> mlua::Error {
|
||||||
mlua::Error::RuntimeError(message.into())
|
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 语法错误转换为用户友好的提示
|
/// 将底层 Lua 语法错误转换为用户友好的提示
|
||||||
pub fn syntax_error(err: mlua::Error) -> mlua::Error {
|
pub fn syntax_error(err: mlua::Error) -> mlua::Error {
|
||||||
match &err {
|
match &err {
|
||||||
@@ -23,3 +37,94 @@ pub fn syntax_error(err: mlua::Error) -> mlua::Error {
|
|||||||
}
|
}
|
||||||
err
|
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>),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// 将 MirrorTarget 构建为 Command 时可能触发的阻断性错误
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum TargetExecutionError {
|
||||||
|
/// 多版本工具未激活版本 (current == None)
|
||||||
|
UnselectedVersion {
|
||||||
|
tool: String,
|
||||||
|
},
|
||||||
|
/// 目标可执行文件物理不存在
|
||||||
|
NotFound {
|
||||||
|
tool: String,
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for TargetExecutionError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::UnselectedVersion { tool } => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"工具 '{}' 当前未指定有效版本。\n提示: 请先运行 'mr use {}' 选择版本。",
|
||||||
|
tool, tool
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Self::NotFound { tool, path } => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"工具 '{}' 的目标程序不存在: {:?}",
|
||||||
|
tool, path
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for TargetExecutionError {}
|
||||||
@@ -1,14 +1,20 @@
|
|||||||
pub mod error;
|
mod error;
|
||||||
mod layout;
|
mod layout;
|
||||||
mod logger;
|
mod logger;
|
||||||
|
mod plugins;
|
||||||
mod runtime;
|
mod runtime;
|
||||||
mod utils;
|
mod utils;
|
||||||
mod validators;
|
mod validators;
|
||||||
|
mod model;
|
||||||
|
|
||||||
pub use layout::Layout;
|
pub use layout::Layout;
|
||||||
// pub use mirror_shim::mirror::Mirror;
|
// pub use mirror_shim::mirror::Mirror;
|
||||||
pub use runtime::LuaRuntime;
|
pub use runtime::LuaRuntime;
|
||||||
|
|
||||||
pub use logger::{init_logging_from, LogLevel, Logger};
|
pub use logger::{LogConfig, LogLevel, init_logging_from};
|
||||||
// pub use mirror_shim::utils::{lua_string_2_os_string, normalize_path_for_lua, parse_tokens};
|
pub use plugins::register_basics;
|
||||||
|
pub use utils::normalize_path_for_lua;
|
||||||
pub use validators::validate_command_name;
|
pub use validators::validate_command_name;
|
||||||
|
// 把 Result 和 Error 提升到根命名空间,给外部带来极致便利
|
||||||
|
pub use error::{Error, Result,validation_error,conversion_err,TargetExecutionError};
|
||||||
|
pub use model::TargetConfig;
|
||||||
+59
-57
@@ -1,7 +1,7 @@
|
|||||||
use crate::Layout;
|
use crate::Layout;
|
||||||
use serde::Deserialize;
|
use std::convert::Infallible;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use tracing_appender::non_blocking::{NonBlocking, WorkerGuard};
|
use tracing_appender::non_blocking::{NonBlocking, WorkerGuard};
|
||||||
use tracing_subscriber::layer::SubscriberExt;
|
use tracing_subscriber::layer::SubscriberExt;
|
||||||
@@ -61,41 +61,50 @@ impl FromStr for LogLevel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Default)]
|
/// 日志配置模型(对应 mirror.ini 的纯强类型表示)
|
||||||
pub struct Logger {
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
#[serde(default)]
|
pub struct LogConfig {
|
||||||
pub level: LogLevel,
|
pub level: LogLevel,
|
||||||
pub log_dir: Option<PathBuf>,
|
pub log_dir: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
impl Logger {
|
impl LogConfig {
|
||||||
/// 零依赖解析 ini / key-value 文本内容
|
/// 零依赖解析 ini / key-value 文本内容
|
||||||
pub fn parse_ini(content: &str) -> Self {
|
/// 计算日志目录的实际物理路径(相对路径基于 base_dir 拼接)
|
||||||
let mut logger = Self::default();
|
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() {
|
for line in content.lines() {
|
||||||
let line = line.trim();
|
let line = line.trim();
|
||||||
// 跳过空行、注释行 (# 或 ;) 和 section 头 ([...])
|
// 跳过空行、注释 (#, ;) 与 section 标头 ([...])
|
||||||
if line.is_empty()
|
if line.is_empty() || line.starts_with(['#', ';', '[']) {
|
||||||
|| line.starts_with('#')
|
|
||||||
|| line.starts_with(';')
|
|
||||||
|| line.starts_with('[')
|
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some((key, val)) = line.split_once('=') {
|
if let Some((key, val)) = line.split_once('=') {
|
||||||
let key = key.trim();
|
let key = key.trim();
|
||||||
let val = val.trim().trim_matches('"').trim_matches('\'');
|
let val = val.trim().trim_matches(['"', '\'']);
|
||||||
|
|
||||||
match key {
|
match key {
|
||||||
"level" => {
|
"level" => {
|
||||||
if let Ok(lvl) = LogLevel::from_str(val) {
|
if let Ok(lvl) = LogLevel::from_str(val) {
|
||||||
logger.level = lvl;
|
config.level = lvl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"log_dir" => {
|
"log_dir" => {
|
||||||
if !val.is_empty() {
|
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 并初始化日志系统。
|
/// 基于 Layout 自动寻址 mirror.toml 并初始化日志系统。
|
||||||
/// 仅在开启详细日志(如 info/debug/trace)时激活异步文件记录并返回 `WorkerGuard`。
|
/// 仅在开启详细日志(如 info/debug/trace)时激活异步文件记录并返回 `WorkerGuard`。
|
||||||
pub fn init_logging_from(layout: &Layout) -> Option<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.toml");
|
||||||
let config_path = layout.base_dir.join("mirror.ini");
|
let config_path = layout.base_dir.join("mirror.ini");
|
||||||
|
|
||||||
// 读取配置文件(如不存在或解析失败,降级回退到默认设置)
|
// 1. 读取并反序列化配置文件(缺失或异常时安全降级为 Default)
|
||||||
let config = if config_path.exists() {
|
let config: LogConfig = fs::read_to_string(&config_path)
|
||||||
fs::read_to_string(&config_path)
|
.ok()
|
||||||
.map(|s| Logger::parse_ini(&s))
|
.and_then(|s| s.parse().ok())
|
||||||
// .ok()
|
.unwrap_or_default();
|
||||||
// .and_then(|s| toml::from_str::<Logger>(&s).ok())
|
|
||||||
.unwrap_or_default()
|
|
||||||
} else {
|
|
||||||
Logger::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
// 1. 优先读取环境变量 MIRROR_LOG_LEVEL,解析失败或未设置则退回到 config.level
|
// 1. 优先读取环境变量 MIRROR_LOG_LEVEL,解析失败或未设置则退回到 config.level
|
||||||
let effective_level = std::env::var("MIRROR_LOG_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);
|
let stderr_layer = fmt::layer().with_writer(std::io::stderr).with_target(false);
|
||||||
|
|
||||||
// 2. 根据强类型枚举判断是否激活落盘模式
|
// 3. 动态配置落盘文件层(仅在 verbose 级别启用)
|
||||||
if effective_level.is_verbose() {
|
let (file_layer, guard) = if effective_level.is_verbose() {
|
||||||
let (non_blocking, guard) = create_file_appender(layout, config.log_dir);
|
let (non_blocking, guard) = create_file_appender(&config.resolve_log_dir(&layout.base_dir));
|
||||||
let file_layer = fmt::layer()
|
let layer = fmt::layer()
|
||||||
.with_writer(non_blocking)
|
.with_writer(non_blocking)
|
||||||
.with_ansi(false)
|
.with_ansi(false)
|
||||||
.with_target(false);
|
.with_target(false);
|
||||||
|
(Some(layer), Some(guard))
|
||||||
|
} else {
|
||||||
|
(None, None)
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. 统一组装并初始化 Subscriber(Option<Layer> 天然实现了 Layer trait)
|
||||||
let _ = tracing_subscriber::registry()
|
let _ = tracing_subscriber::registry()
|
||||||
.with(filter)
|
.with(filter)
|
||||||
.with(stderr_layer)
|
.with(stderr_layer)
|
||||||
.with(file_layer)
|
.with(file_layer)
|
||||||
.try_init();
|
.try_init();
|
||||||
|
|
||||||
Some(guard)
|
guard
|
||||||
} else {
|
|
||||||
let _ = tracing_subscriber::registry()
|
|
||||||
.with(filter)
|
|
||||||
.with(stderr_layer)
|
|
||||||
.try_init();
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_file_appender(
|
fn create_file_appender(output_dir: &Path) -> (NonBlocking, WorkerGuard) {
|
||||||
layout: &Layout,
|
if let Err(err) = fs::create_dir_all(output_dir) {
|
||||||
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"),
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(err) = fs::create_dir_all(&output_dir) {
|
|
||||||
eprintln!("[WARN] 创建日志目录 {} 失败: {}", output_dir.display(), err);
|
eprintln!("[WARN] 创建日志目录 {} 失败: {}", output_dir.display(), err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
use std::ffi::OsString;
|
||||||
|
use mlua::{FromLua, Lua, LuaString, Value};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use crate::utils::lua_string_2_os_string;
|
||||||
|
use crate::validation_error;
|
||||||
|
use crate::validators::ensure_no_nul;
|
||||||
|
|
||||||
|
/// 目标可执行文件的定位配置(支持静态路径与多版本 Table)
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum TargetConfig{
|
||||||
|
/// 静态单版本工具
|
||||||
|
Static(PathBuf),
|
||||||
|
/// 多版本管理
|
||||||
|
Versioned{
|
||||||
|
/// 工具主根目录(供 mr use 扫描候选版本)
|
||||||
|
home:PathBuf,
|
||||||
|
/// 版本真相源文件名(如 ".version",可选,默认 ".version")
|
||||||
|
version_file:Option<String>,
|
||||||
|
/// 当前激活版本的可执行文件完整路径(若未激活或不存在则为 None)
|
||||||
|
current:Option<PathBuf>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Default for TargetConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
TargetConfig::Static(PathBuf::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl TargetConfig {
|
||||||
|
/// 垫片程序(mirror-shim)专用快速通道:提取最终可执行路径
|
||||||
|
///
|
||||||
|
/// - Static: 返回自身路径
|
||||||
|
/// - Versioned: 返回 current 路径(若未激活则返回 None)
|
||||||
|
|
||||||
|
pub fn executable_path(&self) -> Option<&PathBuf> {
|
||||||
|
match self {
|
||||||
|
TargetConfig::Static(path) => Some(path),
|
||||||
|
TargetConfig::Versioned { current, .. } => current.as_ref(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 管理程序(mr-cli)专用辅助方法:判断是否支持多版本
|
||||||
|
pub fn is_versioned(&self) -> bool {
|
||||||
|
matches!(self, TargetConfig::Versioned { .. })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromLua for TargetConfig {
|
||||||
|
fn from_lua(value: Value, _lua: &Lua) -> mlua::Result<Self> {
|
||||||
|
match value {
|
||||||
|
// 分支 1:静态单版本工具 target = "..."
|
||||||
|
Value::String(s) => {
|
||||||
|
let os_str = lua_string_2_os_string(&s)?;
|
||||||
|
ensure_no_nul(&"target", &os_str)?;
|
||||||
|
Ok(TargetConfig::Static(PathBuf::from(os_str)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分支 2:多版本管理工具 target = { home = "...", ... }
|
||||||
|
Value::Table(table) => {
|
||||||
|
// 1. home (必填字段)
|
||||||
|
let home = match table.get::<Option<Value>>("home")? {
|
||||||
|
Some(Value::String(s)) => {
|
||||||
|
let os_str = lua_string_2_os_string(&s)?;
|
||||||
|
ensure_no_nul(&"target.home", &os_str)?;
|
||||||
|
PathBuf::from(os_str)
|
||||||
|
}
|
||||||
|
None | Some(Value::Nil) => {
|
||||||
|
return Err(validation_error("多版本 target 表缺少必填字段 'home'"));
|
||||||
|
}
|
||||||
|
Some(other) => {
|
||||||
|
return Err(validation_error(format!(
|
||||||
|
"target.home 需为路径字符串,实际类型是 {}",
|
||||||
|
other.type_name()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. version_file (选填字段,未填或 nil 时为 None)
|
||||||
|
let version_file = match table.get::<Option<Value>>("version_file")? {
|
||||||
|
Some(Value::String(s)) => {
|
||||||
|
let os_str = lua_string_2_os_string(&s)?;
|
||||||
|
ensure_no_nul(&"target.version_file", &os_str)?;
|
||||||
|
Some(os_str.to_string_lossy().into_owned())
|
||||||
|
}
|
||||||
|
None | Some(Value::Nil) => None,
|
||||||
|
Some(other) => {
|
||||||
|
return Err(validation_error(format!(
|
||||||
|
"target.version_file 需为文件名字符串,实际类型是 {}",
|
||||||
|
other.type_name()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. current (选填/可为 nil,表示未激活或当前未指定版本)
|
||||||
|
let current = match table.get::<Option<Value>>("current")? {
|
||||||
|
Some(Value::String(s)) => {
|
||||||
|
let os_str = lua_string_2_os_string(&s)?;
|
||||||
|
ensure_no_nul(&"target.current", &os_str)?;
|
||||||
|
Some(PathBuf::from(os_str))
|
||||||
|
}
|
||||||
|
// Lua 侧 ver 为 nil 时,current 拼出的也是 nil,此时合法解析为 None
|
||||||
|
None | Some(Value::Nil) => None,
|
||||||
|
Some(other) => {
|
||||||
|
return Err(validation_error(format!(
|
||||||
|
"target.current 需为有效路径字符串或 nil,实际类型是 {}",
|
||||||
|
other.type_name()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(TargetConfig::Versioned {
|
||||||
|
home,
|
||||||
|
version_file,
|
||||||
|
current,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Value::Nil => Err(validation_error(
|
||||||
|
"缺少必填字段 target(应为字符串路径或多版本配置表)",
|
||||||
|
)),
|
||||||
|
other => Err(validation_error(format!(
|
||||||
|
"target 需为有效路径字符串或多版本配置表,实际类型是 {}",
|
||||||
|
other.type_name()
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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(())
|
||||||
|
}
|
||||||
+55
-163
@@ -1,13 +1,34 @@
|
|||||||
use crate::Layout;
|
use crate::{Error, Result};
|
||||||
use crate::error::syntax_error;
|
use mlua::{FromLua, Lua, StdLib, Value};
|
||||||
use crate::utils::normalize_path_for_lua;
|
use std::fs;
|
||||||
use anyhow::{Context, Result};
|
|
||||||
use mlua::{FromLua, Lua, StdLib, Table, Value};
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::{env, fs};
|
use tracing::debug;
|
||||||
use tracing::{debug, trace, warn};
|
|
||||||
|
|
||||||
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 {
|
pub struct LuaRuntime {
|
||||||
lua: Lua,
|
lua: Lua,
|
||||||
@@ -15,43 +36,28 @@ pub struct LuaRuntime {
|
|||||||
|
|
||||||
impl LuaRuntime {
|
impl LuaRuntime {
|
||||||
/// 初始化限定权限的 Lua 沙箱环境
|
/// 初始化限定权限的 Lua 沙箱环境
|
||||||
pub fn new(layout: &Layout) -> Result<Self> {
|
pub fn builder() -> LuaRuntimeBuilder {
|
||||||
// 只加载安全的标准库,剥离 os / io 等风险模块
|
// 只加载安全的标准库,剥离 os / io 等风险模块
|
||||||
let lua = Lua::new_with(
|
let lua = Lua::new_with(
|
||||||
StdLib::TABLE | StdLib::STRING | StdLib::MATH | StdLib::PACKAGE,
|
StdLib::TABLE | StdLib::STRING | StdLib::MATH | StdLib::PACKAGE,
|
||||||
mlua::LuaOptions::default(),
|
mlua::LuaOptions::default(),
|
||||||
)
|
)
|
||||||
.context("初始化 Lua 失败")?;
|
.map_err(Error::LuaInit);
|
||||||
|
|
||||||
// 统一使用 POSIX 风格路径规范化路径字符串
|
LuaRuntimeBuilder { lua }
|
||||||
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 })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 执行指定脚本文件,直接返回完整的 Lua Table
|
/// 执行指定脚本文件,直接返回完整的 Lua Table
|
||||||
pub fn eval_script<T: FromLua>(&self, path: impl AsRef<Path>) -> Result<T> {
|
pub fn eval_script<T: FromLua>(&self, path: impl AsRef<Path>) -> Result<T> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
|
|
||||||
let bytes =
|
let bytes = fs::read(path).map_err(|e| Error::ScriptRead {
|
||||||
fs::read(path).with_context(|| format!("无法读取配置文件: {}", path.display()))?;
|
path: path.to_path_buf(),
|
||||||
|
source: e,
|
||||||
|
})?;
|
||||||
|
|
||||||
let code = String::from_utf8(bytes).with_context(|| {
|
let code = String::from_utf8(bytes).map_err(|_| Error::InvalidUtf8 {
|
||||||
format!(
|
path: path.to_path_buf(),
|
||||||
"{} 不是有效的 UTF-8 文件(请将 Lua 配置文件保存为 UTF-8 编码)",
|
|
||||||
path.display()
|
|
||||||
)
|
|
||||||
})?;
|
})?;
|
||||||
debug!("code {:?}", code);
|
debug!("code {:?}", code);
|
||||||
// 使用 @<path> 格式标记 Chunk 名称,确保 Lua 报错时能精确回溯到对应的源文件名与行号。
|
// 使用 @<path> 格式标记 Chunk 名称,确保 Lua 报错时能精确回溯到对应的源文件名与行号。
|
||||||
@@ -61,150 +67,36 @@ impl LuaRuntime {
|
|||||||
.load(&code)
|
.load(&code)
|
||||||
.set_name(&chunk_name)
|
.set_name(&chunk_name)
|
||||||
.eval::<T>()
|
.eval::<T>()
|
||||||
.map_err(syntax_error)
|
.map_err(|e| Error::LuaRuntime {
|
||||||
.with_context(|| format!("执行 {} 配置文件失败: {}", chunk_name,path.display()))
|
chunk: chunk_name,
|
||||||
}
|
source: e,
|
||||||
|
})
|
||||||
/// 传入注册表的 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(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn require_and_run(&self, module_name: &str, raw_args: Vec<String>) -> Result<()> {
|
pub fn require_and_run(&self, module_name: &str, raw_args: Vec<String>) -> Result<()> {
|
||||||
let require: mlua::Function = self.lua.globals().get("require")?;
|
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 {
|
let func = match handler_val {
|
||||||
mlua::Value::Function(f) => f,
|
Value::Function(f) => f,
|
||||||
other => anyhow::bail!("模块 '{module_name}' 必须返回一个函数,实际为 {}", other.type_name()),
|
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)?;
|
let lua_args = self.lua.create_sequence_from(raw_args)?;
|
||||||
func.call::<()>(lua_args)?;
|
func.call::<()>(lua_args)?;
|
||||||
Ok(())
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{Layout, };
|
use crate::Layout;
|
||||||
|
|
||||||
fn test_layout() -> Layout {
|
fn test_layout() -> Layout {
|
||||||
let root = std::env::temp_dir().join("rshim-test-layout");
|
let root = std::env::temp_dir().join("rshim-test-layout");
|
||||||
@@ -217,7 +109,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn missing_module_require_returns_nil() {
|
fn missing_module_require_returns_nil() {
|
||||||
let runtime = LuaRuntime::new(&test_layout()).unwrap();
|
let runtime = LuaRuntime::builder().build().unwrap();
|
||||||
let value: Value = runtime
|
let value: Value = runtime
|
||||||
.lua
|
.lua
|
||||||
.load(r#"return require("rshim_test_no_such_module")"#)
|
.load(r#"return require("rshim_test_no_such_module")"#)
|
||||||
@@ -228,7 +120,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn builtin_module_require_still_works() {
|
fn builtin_module_require_still_works() {
|
||||||
let runtime = LuaRuntime::new(&test_layout()).unwrap();
|
let runtime = LuaRuntime::builder().build().unwrap();
|
||||||
let value: Value = runtime
|
let value: Value = runtime
|
||||||
.lua
|
.lua
|
||||||
.load(r#"return pcall(require, "string")"#)
|
.load(r#"return pcall(require, "string")"#)
|
||||||
@@ -239,7 +131,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn get_env_returns_split_table() {
|
fn get_env_returns_split_table() {
|
||||||
let runtime = LuaRuntime::new(&test_layout()).unwrap();
|
let runtime = LuaRuntime::builder().build().unwrap();
|
||||||
let value: Value = runtime
|
let value: Value = runtime
|
||||||
.lua
|
.lua
|
||||||
.load(r#"return get_env("PATH")"#)
|
.load(r#"return get_env("PATH")"#)
|
||||||
@@ -266,7 +158,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn get_env_missing_returns_empty_table() {
|
fn get_env_missing_returns_empty_table() {
|
||||||
let runtime = LuaRuntime::new(&test_layout()).unwrap();
|
let runtime = LuaRuntime::builder().build().unwrap();
|
||||||
let value: Value = runtime
|
let value: Value = runtime
|
||||||
.lua
|
.lua
|
||||||
.load(r#"return get_env("RSHIM_TEST_NO_SUCH_VAR_12345")"#)
|
.load(r#"return get_env("RSHIM_TEST_NO_SUCH_VAR_12345")"#)
|
||||||
|
|||||||
@@ -9,3 +9,63 @@ pub fn normalize_path_for_lua(path: &Path) -> String {
|
|||||||
simplified.to_string_lossy().replace('\\', "/")
|
simplified.to_string_lossy().replace('\\', "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn lua_string_2_os_string(s: &LuaString) -> mlua::Result<OsString> {
|
||||||
|
let raw_bytes = &s.as_bytes();
|
||||||
|
// 1. Unix 平台:直接零拷贝透传原始字节(无损支持任意编码)
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::ffi::OsStrExt;
|
||||||
|
Ok(OsStr::from_bytes(raw_bytes).to_os_string())
|
||||||
|
}
|
||||||
|
// 2. Windows 平台:优先按 UTF-8 解码,失败则按当前系统本地代码页 (ANSI/GBK) 转换
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
// 先尝试标准的 UTF-8
|
||||||
|
if let Ok(utf8_str) = std::str::from_utf8(raw_bytes) {
|
||||||
|
return Ok(OsString::from(utf8_str));
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
use std::os::windows::ffi::OsStringExt;
|
||||||
|
use windows_sys::Win32::Globalization::{
|
||||||
|
CP_ACP, MB_ERR_INVALID_CHARS, MultiByteToWideChar,
|
||||||
|
};
|
||||||
|
|
||||||
|
if raw_bytes.is_empty() {
|
||||||
|
return Ok(OsString::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let len = MultiByteToWideChar(
|
||||||
|
CP_ACP,
|
||||||
|
MB_ERR_INVALID_CHARS,
|
||||||
|
raw_bytes.as_ptr(),
|
||||||
|
raw_bytes.len() as i32,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
if len <= 0 {
|
||||||
|
// 构造一个带上下文的 FromLua 转换错误,便于定位配置问题
|
||||||
|
return Err(mlua::Error::FromLuaConversionError {
|
||||||
|
from: "LuaString",
|
||||||
|
to: "OsString".to_string(),
|
||||||
|
message: Some(
|
||||||
|
format!("字符串{:?}包含无效或当前系统无法识别的编码字节", s).to_string(),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buf = vec![0u16; len as usize];
|
||||||
|
MultiByteToWideChar(
|
||||||
|
CP_ACP,
|
||||||
|
MB_ERR_INVALID_CHARS,
|
||||||
|
raw_bytes.as_ptr(),
|
||||||
|
raw_bytes.len() as i32,
|
||||||
|
buf.as_mut_ptr(),
|
||||||
|
len,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(OsString::from_wide(&buf))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use std::fmt;
|
||||||
use crate::error::validation_error;
|
use crate::error::validation_error;
|
||||||
use mlua::Value;
|
use mlua::Value;
|
||||||
|
|
||||||
@@ -39,3 +40,26 @@ pub fn validate_command_name(name: &Value) -> mlua::Result<String> {
|
|||||||
}
|
}
|
||||||
Ok(name_str)
|
Ok(name_str)
|
||||||
}
|
}
|
||||||
|
pub fn ensure_no_nul(context: &impl fmt::Display, os_str: &std::ffi::OsStr) -> mlua::Result<()> {
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::ffi::OsStrExt;
|
||||||
|
if os_str.as_bytes().contains(&0) {
|
||||||
|
return Err(conversion_error(format!(
|
||||||
|
"{} 的值不能包含 NUL 字符",
|
||||||
|
context
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
use std::os::windows::ffi::OsStrExt;
|
||||||
|
if os_str.encode_wide().any(|c| c == 0) {
|
||||||
|
return Err(validation_error(format!(
|
||||||
|
"{} 的值不能包含 NUL 字符",
|
||||||
|
context
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
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, TargetConfig, TargetExecutionError, 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. 提取生效路径,缺失版本时直接抛出 UnselectedVersion
|
||||||
|
let target_path = match mr.target.executable_path() {
|
||||||
|
Some(path) => path,
|
||||||
|
None => {
|
||||||
|
warn!(
|
||||||
|
"[mirror] 工具 '{}' 当前未指定生效版本。\n提示: 请先运行 'mr use {}' 选择版本。",
|
||||||
|
target_name, 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(target_path, 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(target_path, &elevated_args, Some(&mr.env)))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!(
|
||||||
|
"启动目标程序 [{}] 时发生错误: {}",
|
||||||
|
target_path.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!(
|
||||||
|
"等待目标程序 [{}] 执行完毕时发生错误: {}",
|
||||||
|
target_path.to_string_lossy(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
EXIT_FAILED_WAIT_PROG
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-57
@@ -1,15 +1,14 @@
|
|||||||
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 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 mirror;
|
||||||
pub mod validators;
|
pub mod sys;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
pub mod validators;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let current_exe = match env::current_exe().context("获取代理程序路径失败") {
|
let current_exe = match env::current_exe().context("获取代理程序路径失败") {
|
||||||
@@ -19,6 +18,7 @@ fn main() {
|
|||||||
exit(EXIT_FAILED_LOAD_SHIM);
|
exit(EXIT_FAILED_LOAD_SHIM);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let layout = match Layout::from(¤t_exe) {
|
let layout = match Layout::from(¤t_exe) {
|
||||||
Ok(l) => l,
|
Ok(l) => l,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -27,7 +27,6 @@ fn main() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
//初始化日志:输出到 stderr,避免污染 shim 子进程的 stdout
|
//初始化日志:输出到 stderr,避免污染 shim 子进程的 stdout
|
||||||
let _guard = init_logging_from(&layout);
|
let _guard = init_logging_from(&layout);
|
||||||
|
|
||||||
@@ -41,52 +40,6 @@ fn main() {
|
|||||||
// 2. 注册 Windows 控制台信号
|
// 2. 注册 Windows 控制台信号
|
||||||
set_console_ctrl_handler();
|
set_console_ctrl_handler();
|
||||||
|
|
||||||
// 3. 解析调用参数与代理 Mirror 配置
|
let code = executor::run(&layout);
|
||||||
let calling_args: Vec<_> = env::args_os().skip(1).collect();
|
exit(code);
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|||||||
+51
-67
@@ -1,96 +1,75 @@
|
|||||||
use mirror_core::error::validation_error;
|
|
||||||
use crate::validators::LuaValidator;
|
use crate::validators::LuaValidator;
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
|
use mirror_core::validate_command_name;
|
||||||
|
use mirror_core::{Layout, LuaRuntime, TargetConfig};
|
||||||
|
use mirror_core::{TargetExecutionError, conversion_err, validation_error};
|
||||||
use mlua::{FromLua, Lua, Table, Value};
|
use mlua::{FromLua, Lua, Table, Value};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use tracing::{debug, trace};
|
use tracing::{debug, trace};
|
||||||
use mirror_core::{Layout, LuaRuntime};
|
|
||||||
use mirror_core::validate_command_name;
|
|
||||||
|
|
||||||
struct MasterMirror{
|
pub struct MirrorRegistry {
|
||||||
master:HashMap<String,Mirror>,
|
pub targets: HashMap<String, MirrorTarget>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromLua for MasterMirror {
|
impl FromLua for MirrorRegistry {
|
||||||
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> {
|
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> {
|
||||||
let root_tbl = match value {
|
let root_tbl = match value {
|
||||||
Value::Table(t) => t,
|
Value::Table(t) => t,
|
||||||
other => {
|
other => {
|
||||||
return Err(validation_error(format!(
|
return Err(conversion_err(
|
||||||
"mirror 注册表的顶层配置必须是 Table,实际检测到: {}",
|
other.type_name(),
|
||||||
|
"MirrorRegistry",
|
||||||
|
format!(
|
||||||
|
"mirror 全局注册表顶层必须是 Table,实际检测到: {}",
|
||||||
other.type_name()
|
other.type_name()
|
||||||
)));
|
),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut master = HashMap::new();
|
let mut targets = HashMap::new();
|
||||||
for pair in root_tbl.pairs::<Value, Value>() {
|
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
|
// 2. 校验 entry 是否是 Table
|
||||||
if !cmd_entry.is_table() {
|
if !entry.is_table() {
|
||||||
return Err(validation_error(format!(
|
return Err(conversion_err(
|
||||||
"子命令 '{cmd_name}' 的配置必须是 Table,实际检测到: {}",
|
entry.type_name(),
|
||||||
cmd_entry.type_name()
|
"MirrorRegistry",
|
||||||
)));
|
format!(
|
||||||
|
"目标工具 '{target_name}' 的配置必须是 Table,实际检测到: {}",
|
||||||
|
entry.type_name()
|
||||||
|
),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
// 3. 直接交由子命令解析,外层负责补充错误上下文
|
// 3. 直接交由子命令解析,外层负责补充错误上下文
|
||||||
let cmd_def = Mirror::from_lua(cmd_entry, lua).map_err(|err| {
|
let target_spec = MirrorTarget::from_lua(entry, lua).map_err(|err| {
|
||||||
validation_error(format!("子命令 '{cmd_name}' 配置解析失败:\n{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)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct Mirror {
|
pub struct MirrorTarget {
|
||||||
pub target: PathBuf,
|
/// 目标程序可执行文件路径
|
||||||
|
pub target: TargetConfig,
|
||||||
|
/// 默认预设参数列表
|
||||||
pub args: Vec<OsString>,
|
pub args: Vec<OsString>,
|
||||||
|
/// 参数别名映射表(用于展开以 mr: 为前缀的别名)
|
||||||
pub aliases: HashMap<String, Vec<OsString>>,
|
pub aliases: HashMap<String, Vec<OsString>>,
|
||||||
|
/// 注入/覆盖的环境变量
|
||||||
pub env: HashMap<String, OsString>,
|
pub env: HashMap<String, OsString>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Mirror {
|
impl MirrorTarget {
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 1. 核心参数路由解析(Delegation 到 Spec 的路由逻辑)
|
/// 1. 核心参数路由解析(Delegation 到 Spec 的路由逻辑)
|
||||||
fn resolve_args<I, S>(&self, args: I) -> Vec<OsString>
|
fn resolve_args<I, S>(&self, args: I) -> Vec<OsString>
|
||||||
where
|
where
|
||||||
@@ -125,12 +104,13 @@ impl Mirror {
|
|||||||
expanded_args
|
expanded_args
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_command<I, S>(&self, combined_args: I) -> Command
|
pub fn to_command<P, I, S>(&self, target: P, combined_args: I) -> Command
|
||||||
where
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
I: IntoIterator<Item = S>,
|
I: IntoIterator<Item = S>,
|
||||||
S: AsRef<std::ffi::OsStr>,
|
S: AsRef<std::ffi::OsStr>,
|
||||||
{
|
{
|
||||||
let mut cmd = Command::new(&self.target);
|
let mut cmd = Command::new(target.as_ref());
|
||||||
|
|
||||||
// 2. 传入预打平的别名字典,查表并展开所有以 `mr:` 为前缀的别名
|
// 2. 传入预打平的别名字典,查表并展开所有以 `mr:` 为前缀的别名
|
||||||
let final_args = self.resolve_args(combined_args);
|
let final_args = self.resolve_args(combined_args);
|
||||||
@@ -147,8 +127,9 @@ impl Mirror {
|
|||||||
cmd
|
cmd
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl FromLua for Mirror {
|
|
||||||
fn from_lua(value: Value, _lua: &Lua) -> mlua::Result<Self> {
|
impl FromLua for MirrorTarget {
|
||||||
|
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> {
|
||||||
// 脚本返回必须是一个 Table 变体
|
// 脚本返回必须是一个 Table 变体
|
||||||
let table = match value {
|
let table = match value {
|
||||||
Value::Table(t) => t,
|
Value::Table(t) => t,
|
||||||
@@ -163,9 +144,12 @@ impl FromLua for Mirror {
|
|||||||
// 必填字段: target(严格限定为字符串,避免数字被 mlua 宽松转为字符串后掩盖错误)
|
// 必填字段: target(严格限定为字符串,避免数字被 mlua 宽松转为字符串后掩盖错误)
|
||||||
let target = match table.get::<Option<Value>>("target")? {
|
let target = match table.get::<Option<Value>>("target")? {
|
||||||
None | Some(Value::Nil) => {
|
None | Some(Value::Nil) => {
|
||||||
return Err(validation_error("缺少必填字段 target(应为字符串路径)"));
|
return Err(validation_error(
|
||||||
|
"缺少必填字段 target(应为字符串路径或多版本配置表)",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
Some(target_val) => LuaValidator::parse_target(&target_val)?,
|
// Some(target_val) => LuaValidator::parse_target(&target_val)?,
|
||||||
|
Some(target_val) => TargetConfig::from_lua(target_val, lua)?,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 可选字段: args(缺失或 nil 时默认空列表;保留空字符串参数,与提权路径的 "" 语义一致)
|
// 可选字段: args(缺失或 nil 时默认空列表;保留空字符串参数,与提权路径的 "" 语义一致)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use mirror_core::error::validation_error;
|
|
||||||
use anyhow::{Context, Result, anyhow};
|
use anyhow::{Context, Result, anyhow};
|
||||||
|
use mirror_core::{TargetConfig, validation_error};
|
||||||
use mlua::{LuaString, Table, Value};
|
use mlua::{LuaString, Table, Value};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
@@ -7,8 +7,8 @@ use std::fmt;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
// use tinyjson::JsonValue;
|
// use tinyjson::JsonValue;
|
||||||
use tracing::debug;
|
|
||||||
use crate::utils::{lua_string_2_os_string, parse_tokens};
|
use crate::utils::{lua_string_2_os_string, parse_tokens};
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
/// Lua 值校验器:针对不同上下文定义校验规则
|
/// Lua 值校验器:针对不同上下文定义校验规则
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -212,17 +212,79 @@ impl LuaValidator {
|
|||||||
}
|
}
|
||||||
impl LuaValidator {
|
impl LuaValidator {
|
||||||
/// 解析并校验 `target`
|
/// 解析并校验 `target`
|
||||||
pub fn parse_target(value: &Value) -> mlua::Result<PathBuf> {
|
pub fn parse_target(value: &Value) -> mlua::Result<TargetConfig> {
|
||||||
let ctx = Self::Target;
|
let ctx = Self::Target;
|
||||||
match value {
|
match value {
|
||||||
Value::String(s) => {
|
Value::String(s) => {
|
||||||
let os_str = lua_string_2_os_string(&s)?;
|
let os_str = lua_string_2_os_string(&s)?;
|
||||||
Self::ensure_no_nul(&ctx, &os_str)?;
|
Self::ensure_no_nul(&ctx, &os_str)?;
|
||||||
Ok(PathBuf::from(os_str))
|
Ok(TargetConfig::Static(PathBuf::from(os_str)))
|
||||||
}
|
}
|
||||||
Value::Nil => Err(validation_error("缺少必填字段 target(应为字符串路径)")),
|
// 分支 2:多版本动态工具 target = { home = "...", version_file = "...", current = "..." }
|
||||||
|
Value::Table(t) => {
|
||||||
|
// 1. 解析 home(必填)
|
||||||
|
let home_val = t.get::<Value>("home")?;
|
||||||
|
let home = match home_val {
|
||||||
|
Value::String(ref s) => {
|
||||||
|
let os_str = lua_string_2_os_string(s)?;
|
||||||
|
Self::ensure_no_nul(&"target.home", &os_str)?;
|
||||||
|
PathBuf::from(os_str)
|
||||||
|
}
|
||||||
|
Value::Nil => {
|
||||||
|
return Err(validation_error("多版本 target 表缺少必填字段 'home'"));
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
return Err(validation_error(format!(
|
||||||
|
"target.home 需为有效路径字符串,实际类型是 {}",
|
||||||
|
other.type_name()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. 解析 version_file(选填,默认为 None,后续业务可 fallback 到 .version)
|
||||||
|
let version_file = match t.get::<Option<Value>>("version_file")? {
|
||||||
|
Some(Value::String(ref s)) => {
|
||||||
|
let os_str = lua_string_2_os_string(s)?;
|
||||||
|
Self::ensure_no_nul(&"target.version_file", &os_str)?;
|
||||||
|
Some(os_str.to_string_lossy().into_owned())
|
||||||
|
}
|
||||||
|
Some(Value::Nil) | None => None,
|
||||||
|
Some(other) => {
|
||||||
|
return Err(validation_error(format!(
|
||||||
|
"target.version_file 应为文件名字符串,实际类型是 {}",
|
||||||
|
other.type_name()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. 解析 current(选填/可为 nil:未激活或找不到版本时允许为 None)
|
||||||
|
let current = match t.get::<Option<Value>>("current")? {
|
||||||
|
Some(Value::String(ref s)) => {
|
||||||
|
let os_str = lua_string_2_os_string(s)?;
|
||||||
|
Self::ensure_no_nul(&"target.current", &os_str)?;
|
||||||
|
Some(PathBuf::from(os_str))
|
||||||
|
}
|
||||||
|
// Lua 侧 ver 为 nil 时,current 计算结果即为 nil,合法放行
|
||||||
|
Some(Value::Nil) | None => None,
|
||||||
|
Some(other) => {
|
||||||
|
return Err(validation_error(format!(
|
||||||
|
"target.current 需为有效路径字符串或 nil,实际类型是 {}",
|
||||||
|
other.type_name()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(TargetConfig::Versioned {
|
||||||
|
home,
|
||||||
|
version_file,
|
||||||
|
current,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Value::Nil => Err(validation_error(
|
||||||
|
"缺少必填字段 target(应为字符串路径或多版本配置表)",
|
||||||
|
)),
|
||||||
other => Err(validation_error(format!(
|
other => Err(validation_error(format!(
|
||||||
"{} 需为有效的路径且类型必须是字符串,实际类型是 {}",
|
"{} 需为有效的路径且类型必须是字符串或多版本配置表,实际类型是 {}",
|
||||||
ctx,
|
ctx,
|
||||||
other.type_name()
|
other.type_name()
|
||||||
))),
|
))),
|
||||||
@@ -471,10 +533,10 @@ impl LuaValidator {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::Mirror;
|
use crate::mirror::MirrorTarget;
|
||||||
use mlua::{FromLua, Lua};
|
use mlua::{FromLua, Lua};
|
||||||
|
|
||||||
fn parse(src: &str) -> mlua::Result<Mirror> {
|
fn parse(src: &str) -> mlua::Result<MirrorTarget> {
|
||||||
let lua = Lua::new();
|
let lua = Lua::new();
|
||||||
// 模拟 runtime 注入的 get_env:返回按平台分隔符拆分的段数组(空变量返回空表)
|
// 模拟 runtime 注入的 get_env:返回按平台分隔符拆分的段数组(空变量返回空表)
|
||||||
let get_env = lua
|
let get_env = lua
|
||||||
@@ -493,7 +555,7 @@ mod tests {
|
|||||||
lua.globals().set("get_env", get_env).unwrap();
|
lua.globals().set("get_env", get_env).unwrap();
|
||||||
|
|
||||||
let value = lua.load(src).eval::<Value>()?;
|
let value = lua.load(src).eval::<Value>()?;
|
||||||
let t = Mirror::from_lua(value, &lua);
|
let t = MirrorTarget::from_lua(value, &lua);
|
||||||
println!("读取出的数据:{:?}", t.clone()?);
|
println!("读取出的数据:{:?}", t.clone()?);
|
||||||
t
|
t
|
||||||
}
|
}
|
||||||
@@ -534,7 +596,10 @@ mod tests {
|
|||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(cfg.target, PathBuf::from("C:/tools/git.exe"));
|
assert_eq!(
|
||||||
|
cfg.target.executable_path().unwrap().to_path_buf(),
|
||||||
|
PathBuf::from("C:/tools/git.exe")
|
||||||
|
);
|
||||||
// assert_eq!(cfg.args, vec!["--no-pager", "2"]);
|
// assert_eq!(cfg.args, vec!["--no-pager", "2"]);
|
||||||
assert_eq!(cfg.env.get("HOME").unwrap().to_str(), Some("C:/tools/home"));
|
assert_eq!(cfg.env.get("HOME").unwrap().to_str(), Some("C:/tools/home"));
|
||||||
// PATH 前缀来自配置,随后附加 get_env("PATH") 拆出的宿主 PATH 段
|
// PATH 前缀来自配置,随后附加 get_env("PATH") 拆出的宿主 PATH 段
|
||||||
|
|||||||
Reference in New Issue
Block a user