refactor(core,shim): 统一基于 thiserror 的错误体系
- 移除 anyhow,使用 thiserror 定义强类型 Error 枚举 - 支持 #[from] 自动转换 mlua::Error,关键路径保留源码与上下文 - LuaRuntimeBuilder 采用惰性错误收集,简化链式初始化调用 - MirrorRegistry 实现规范化 FromLuaConversionError 错误转换
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
name = "mirror-cli"
|
||||
version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
description = "Mirror CLI Manager"
|
||||
# 显式指定生成的二进制文件名
|
||||
[[bin]]
|
||||
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 {
|
||||
let mut app = Command::new("mr")
|
||||
.about("Mirror CLI Manager")
|
||||
.version("0.1.0")
|
||||
.about(env!("CARGO_PKG_DESCRIPTION"))
|
||||
.version(env!("CARGO_PKG_VERSION"))
|
||||
.arg_required_else_help(true)
|
||||
.subcommand(log_subcommand());
|
||||
|
||||
@@ -28,28 +28,20 @@ pub fn build(dynamic_cmds: MrCommand) -> Command {
|
||||
fn log_subcommand() -> Command {
|
||||
Command::new("log")
|
||||
.about("查看或修改 mirror.ini 日志配置")
|
||||
.arg(
|
||||
Arg::new("tail")
|
||||
.short('t')
|
||||
.long("tail")
|
||||
.value_name("LINES")
|
||||
.help("显示最近的 N 行日志")
|
||||
.value_parser(clap::value_parser!(usize)),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("level")
|
||||
.short('l')
|
||||
.long("level")
|
||||
.value_name("LEVEL")
|
||||
.help("设置运行时日志级别")
|
||||
.help("设置日志级别 (off, error, warn, info, debug, trace)")
|
||||
.value_parser(clap::value_parser!(LogLevel)),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("follow")
|
||||
.short('f')
|
||||
.long("follow")
|
||||
.help("持续跟踪日志输出 (tail -f)")
|
||||
.action(ArgAction::SetTrue),
|
||||
Arg::new("dir")
|
||||
.short('d')
|
||||
.long("dir")
|
||||
.value_name("PATH")
|
||||
.help("设置日志存放路径(绝对路径或相对于 base_dir 的相对路径)"),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +1,58 @@
|
||||
use anyhow::{Context, Result};
|
||||
use mirror_core::{Layout, LogLevel};
|
||||
use mirror_core::{Layout, LogConfig, LogLevel};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
// Rust 原生处理 log 命令逻辑
|
||||
pub(crate) fn log_handle(layout: &Layout, matches: &clap::ArgMatches) -> Result<()> {
|
||||
let log_ini_path = layout.base_dir.join("../../../mirror-core/mirror.ini");
|
||||
let log_ini_path = layout.base_dir.join("mirror.ini");
|
||||
|
||||
// 1. 处理设置日志等级
|
||||
if let Some(&level) = matches.get_one::<LogLevel>("level") {
|
||||
let content = format!("level = \"{}\"\nlog_dir = \"logs\"\n", level.as_str());
|
||||
fs::write(&log_ini_path, content)
|
||||
.with_context(|| format!("写入日志配置文件失败: {}", log_ini_path.display()))?;
|
||||
println!("日志级别已更新为: {level} ({})", log_ini_path.display());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 2. 处理 tail 查看
|
||||
if let Some(&tail_lines) = matches.get_one::<usize>("tail") {
|
||||
println!("正在检索最后 {tail_lines} 行日志...");
|
||||
// 原生 Rust 高性能按行倒序读取日志文件
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 3. 默认打印当前配置
|
||||
if log_ini_path.is_file() {
|
||||
let current_ini = fs::read_to_string(&log_ini_path)
|
||||
let new_level = matches.get_one::<LogLevel>("level").copied();
|
||||
let new_dir = matches.get_one::<String>("dir").map(|s| s.trim());
|
||||
// 1. 反序列化:读取并解析现有配置;若文件不存在则使用默认配置
|
||||
let mut config: LogConfig = if log_ini_path.is_file() {
|
||||
let content = fs::read_to_string(&log_ini_path)
|
||||
.with_context(|| format!("读取配置文件失败: {}", log_ini_path.display()))?;
|
||||
println!("当前 mirror.ini 配置:\n{current_ini}");
|
||||
content.parse().unwrap_or_default()
|
||||
} else {
|
||||
println!("未找到 mirror.ini,当前使用默认全局级别: warn");
|
||||
LogConfig::default()
|
||||
};
|
||||
// 2. 修改配置并写盘(仅更新用户指定的字段,未指定的字段保留原值)
|
||||
if new_level.is_some() || new_dir.is_some() {
|
||||
if let Some(lvl) = new_level {
|
||||
config.level = lvl;
|
||||
}
|
||||
if let Some(dir) = new_dir {
|
||||
config.log_dir = Some(PathBuf::from(dir));
|
||||
}
|
||||
|
||||
// 序列化写回
|
||||
fs::write(&log_ini_path, config.to_string())
|
||||
.with_context(|| format!("写入日志配置文件失败: {}", log_ini_path.display()))?;
|
||||
|
||||
let actual_dir = config.resolve_log_dir(&layout.base_dir);
|
||||
println!("✔ 日志配置已更新:");
|
||||
println!(" 日志级别: {}", config.level.as_str());
|
||||
println!(" 日志目录: {}", actual_dir.display());
|
||||
println!(" 配置文件: {}", log_ini_path.display());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 3. 无修改参数时,展示当前配置状态
|
||||
println!("=== Mirror 日志配置 ===");
|
||||
println!("配置文件: {}", log_ini_path.display());
|
||||
println!("---------------------");
|
||||
|
||||
if log_ini_path.is_file() {
|
||||
print!("{config}");
|
||||
} else {
|
||||
println!("状态: 配置文件尚未创建");
|
||||
println!("当前生效级别: {} (默认)", config.level.as_str());
|
||||
println!(
|
||||
"默认日志目录: {}",
|
||||
config.resolve_log_dir(&layout.base_dir).display()
|
||||
);
|
||||
println!("\n提示: 可使用 'mr log -l debug' 或 'mr log -d my_logs' 进行配置修改");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::validators;
|
||||
use mirror_core::error::validation_error;
|
||||
use mirror_core::{conversion_err, validation_error};
|
||||
use mlua::{FromLua, Lua, Value};
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -20,10 +20,14 @@ impl FromLua for MrCommand {
|
||||
let root_tbl = match value {
|
||||
Value::Table(t) => t,
|
||||
other => {
|
||||
return Err(validation_error(format!(
|
||||
"commands 注册表的顶层配置必须是 Table,实际检测到: {}",
|
||||
other.type_name()
|
||||
)));
|
||||
return Err(conversion_err(
|
||||
other.type_name(),
|
||||
"MrCommand",
|
||||
format!(
|
||||
"commands 注册表的顶层配置必须是 Table,实际检测到: {}",
|
||||
other.type_name()
|
||||
),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -35,10 +39,14 @@ impl FromLua for MrCommand {
|
||||
let cmd_name = validators::validate_command_name(&cmd_name)?;
|
||||
// 2. 校验 entry 是否是 Table
|
||||
if !cmd_entry.is_table() {
|
||||
return Err(validation_error(format!(
|
||||
"子命令 '{cmd_name}' 的配置必须是 Table,实际检测到: {}",
|
||||
cmd_entry.type_name()
|
||||
)));
|
||||
return Err(conversion_err(
|
||||
cmd_entry.type_name(),
|
||||
"MrCommand",
|
||||
format!(
|
||||
"子命令 '{cmd_name}' 的配置必须是 Table,实际检测到: {}",
|
||||
cmd_entry.type_name()
|
||||
),
|
||||
));
|
||||
}
|
||||
// 3. 直接交由子命令解析,外层负责补充错误上下文
|
||||
let cmd_def = MrSubCommand::from_lua(cmd_entry, lua).map_err(|err| {
|
||||
@@ -55,7 +63,7 @@ pub struct MrSubCommand {
|
||||
/// 命令处理函数:必须是 Lua Function
|
||||
pub(crate) module: String,
|
||||
/// 命令行说明:必须是普通文本字符串
|
||||
pub(crate) description: String ,
|
||||
pub(crate) description: String,
|
||||
}
|
||||
|
||||
impl FromLua for MrSubCommand {
|
||||
@@ -63,10 +71,11 @@ impl FromLua for MrSubCommand {
|
||||
let table = match value {
|
||||
Value::Table(t) => t,
|
||||
other => {
|
||||
return Err(validation_error(format!(
|
||||
"配置必须是 Table,实际检测到: {}",
|
||||
other.type_name()
|
||||
)));
|
||||
return Err(conversion_err(
|
||||
other.type_name(),
|
||||
"MrSubCommand",
|
||||
format!("配置必须是 Table,实际检测到: {}", other.type_name()),
|
||||
));
|
||||
}
|
||||
};
|
||||
// validators::LuaValidator::parse_command_entry(&tbl)
|
||||
@@ -83,7 +92,7 @@ impl FromLua for MrSubCommand {
|
||||
)));
|
||||
}
|
||||
}
|
||||
let handler_val = table.get::<Value>("handler")?;
|
||||
let module_val = table.get::<Value>("module")?;
|
||||
// let handler_func = match handler_val {
|
||||
// Value::Function(f) => f,
|
||||
// Value::Boolean(true) => {
|
||||
@@ -105,14 +114,15 @@ impl FromLua for MrSubCommand {
|
||||
// )));
|
||||
// }
|
||||
// };
|
||||
let module = match handler_val {
|
||||
Value::Table(t) if t.get::<bool>("__is_lazy_command").unwrap_or(false) => {
|
||||
t.get::<String>("module")?
|
||||
let module = match module_val {
|
||||
Value::String(s) => {
|
||||
// 兜底归一化(防止用户手写 module = "a/b" 时未走 mr.command)
|
||||
// 同时兼容直接写字符串的情况
|
||||
s.to_str()?.replace(['/', '\\'], ".")
|
||||
}
|
||||
Value::String(s) => s.to_str()?.to_string(), // 同时兼容直接写字符串的情况
|
||||
other => {
|
||||
return Err(mlua::Error::runtime(format!(
|
||||
"handler 必须通过 command(\"xxx\") 声明,实际为 {}",
|
||||
"module 必须通过 mr.command(\"xxx\") 声明,实际为 {}",
|
||||
other.type_name()
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
+38
-8
@@ -1,14 +1,44 @@
|
||||
mod app;
|
||||
use crate::executor::EXIT_SUCCESS;
|
||||
use anyhow::Context;
|
||||
use mirror_core::{Layout, init_logging_from};
|
||||
use std::process::exit;
|
||||
use tracing::{debug, error};
|
||||
|
||||
mod cli;
|
||||
mod commands;
|
||||
mod error;
|
||||
mod executor;
|
||||
mod plugins;
|
||||
mod validators;
|
||||
|
||||
use std::process::ExitCode;
|
||||
fn main() {
|
||||
let current_exe = match std::env::current_exe().context("获取代理程序路径失败") {
|
||||
Ok(exe) => exe,
|
||||
Err(e) => {
|
||||
eprintln!("垫片启动失败: {:#}", e);
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
let layout = match Layout::from(¤t_exe) {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
eprintln!("解析程序目录布局失败: {:#}", e);
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
let _guard = init_logging_from(&layout);
|
||||
|
||||
fn main() -> ExitCode {
|
||||
if let Err(err) = app::run() {
|
||||
eprintln!("\x1b[31m[mr error]\x1b[0m {:#}", err);
|
||||
return ExitCode::FAILURE; // 返回退出码 1
|
||||
debug!("=== 管理程序启动 ===");
|
||||
debug!(
|
||||
exe = %current_exe.display(),
|
||||
target_name = %layout.target_name,
|
||||
"日志系统与布局上下文已就位"
|
||||
);
|
||||
// 核心运行:成功退出 0,失败根据 AppError 里的 code 退出
|
||||
if let Err(app_err) = executor::run(&layout) {
|
||||
error!("{:#}", app_err.source);
|
||||
exit(app_err.code);
|
||||
}
|
||||
ExitCode::SUCCESS // 返回退出码 0
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
pub fn validate_command_name(name: &Value) -> mlua::Result<String> {
|
||||
|
||||
Reference in New Issue
Block a user