feat(cli): 优化 log 子命令并引入 LogLevel 强类型解析,重构 commands.lua 注册表解析与校验逻辑
- 调整 LogLevel 的 FromStr 错误类型为 String,适配 clap 的 value_parser - log 子命令直接绑定 LogLevel 枚举,消除硬编码校验与类型转换 - 规范化日志级别更新与配置读取逻辑 - handler 类型有 Function 改为 String
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,3 +3,5 @@
|
||||
/.idea
|
||||
*.exe
|
||||
./Cargo.lock
|
||||
**/target
|
||||
**/Cargo.lock
|
||||
|
||||
181
Cargo.lock
generated
181
Cargo.lock
generated
@@ -11,6 +11,56 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.104"
|
||||
@@ -41,9 +91,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.4.3"
|
||||
version = "1.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d"
|
||||
checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
@@ -55,6 +105,52 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.16"
|
||||
@@ -84,9 +180,9 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.17.0"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
|
||||
checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
@@ -94,6 +190,18 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
@@ -123,18 +231,18 @@ checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||
|
||||
[[package]]
|
||||
name = "lua-src"
|
||||
version = "550.1.1"
|
||||
version = "551.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75c110c2fa33f34e0de05448e1f3eb2e0631e7a69e2d8ae1586cffc9fc9f9949"
|
||||
checksum = "087097f9936a7d819bda525b32d6e96f9c54f3d35ff23ff72fc0c1697d2127db"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "luajit-src"
|
||||
version = "210.7.2+b925b3e"
|
||||
version = "210.7.3+1ee778a"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "920cf654b23d217c550ceea57c32cd2a413ea27b6d47ed77b5ee0cf655adefa6"
|
||||
checksum = "869665372263eb337b14f480cfb864b89f12eade4eb42cb415f71517b4a67572"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"which",
|
||||
@@ -156,7 +264,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "mirror"
|
||||
name = "mirror-cli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"clap_derive",
|
||||
"mirror-core",
|
||||
"mlua",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mirror-core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
@@ -170,11 +291,23 @@ dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mirror-shim"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"mirror-core",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mlua"
|
||||
version = "0.12.0"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad72ffa037cf5970c9860674f32f703fda25d86cf217475fe7a79c5f9961bcaa"
|
||||
checksum = "d96e5d00f19d8c46c71ceaced99593b90c31c57aa1fe2cb3e93a8b1698eedba9"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"either",
|
||||
@@ -187,9 +320,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mlua-sys"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92136787b906d4e55cfe96cd6c62e010bb1a56889d0d6cf83eb016dbad07576b"
|
||||
checksum = "b806d7ade031f5d6607eae3e283fb034cb795a76247dd0d1ba753c8c42debccf"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
@@ -229,6 +362,12 @@ version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
@@ -377,6 +516,12 @@ version = "1.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "symlink"
|
||||
version = "0.1.0"
|
||||
@@ -550,6 +695,12 @@ version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
@@ -558,9 +709,9 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "8.0.5"
|
||||
version = "8.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f3ef584124b911bcc3875c2f1472e80f24361ceb789bd1c62b3e9a3df9ff43c"
|
||||
checksum = "bae2f2b2b816647a1cab1acc91f5bd20812d53cb344382635ec2181940c8034f"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
23
Cargo.toml
23
Cargo.toml
@@ -1,16 +1,21 @@
|
||||
[package]
|
||||
name = "mirror"
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"mirror-core", "mirror-cli",
|
||||
"mirror-shim",
|
||||
]
|
||||
|
||||
#[package]
|
||||
#name = "mirror"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
rust-version = "1.94"
|
||||
license = "MIT OR Unlicense"
|
||||
description = "A fast, safe Rust shim launcher for Scoop"
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
panic = "abort"
|
||||
|
||||
[dependencies]
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.104"
|
||||
|
||||
mlua = { version = "0.12.0", features = ["lua54", "vendored","send"] }
|
||||
@@ -35,7 +40,3 @@ tinyjson="2.5.1"
|
||||
#toml = "1.1.4+spec-1.1.0"
|
||||
|
||||
|
||||
|
||||
[features]
|
||||
default = []
|
||||
args=[]
|
||||
37
commands/commands.lua
Normal file
37
commands/commands.lua
Normal file
@@ -0,0 +1,37 @@
|
||||
-- commands/commands.lua
|
||||
function command(mod_path)
|
||||
-- 无论输入 "a/b/c" 还是 "a\b\c",统一替换为标准的 "a.b.c"
|
||||
local normalized = mod_path:gsub("[/\\]", ".")
|
||||
|
||||
return {
|
||||
__is_lazy_command = true,
|
||||
module = normalized
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
return {
|
||||
-- 1. 单文件子命令 (commands/use.lua)
|
||||
["use"] = {
|
||||
description = "Switch the active version of a tool",
|
||||
handler = command("use")
|
||||
},
|
||||
|
||||
-- 2. 单文件子命令 (commands/list.lua)
|
||||
["list"] = {
|
||||
description = "List available and currently activated versions",
|
||||
handler = command("list")
|
||||
},
|
||||
|
||||
-- 3. 复杂多文件子命令 (加载 commands/log/init.lua)
|
||||
["log"] = {
|
||||
description = "View or configure mirror runtime logs",
|
||||
handler = command("log")
|
||||
},
|
||||
|
||||
-- 4. 支持别名快捷映射 (例如 mr ls 等价于 mr list)
|
||||
["ls"] = {
|
||||
description = "Alias for list",
|
||||
handler = command("list")
|
||||
}
|
||||
}
|
||||
24
commands/log/init.lua
Normal file
24
commands/log/init.lua
Normal file
@@ -0,0 +1,24 @@
|
||||
-- commands/log/init.lua
|
||||
local argparse = require("argparse")
|
||||
local utils = require("log.utils") -- 支持相对目录级 require
|
||||
|
||||
return function(raw_args)
|
||||
local parser = argparse("mr log", "Manage and view mirror logs.")
|
||||
parser:option("-l --level", "Set log level"):choices({"trace", "debug", "info", "warn", "error"})
|
||||
parser:option("-t --tail", "Show last N lines"):convert(tonumber)
|
||||
|
||||
local ok, args = pcall(function() return parser:parse(raw_args) end)
|
||||
if not ok then
|
||||
error("__ARGPARSE_HELP__")
|
||||
end
|
||||
|
||||
if args.level then
|
||||
print(utils.format_level(args.level))
|
||||
mr.fs.write(__MIRROR_DIR__ .. "/mirror-log.ini", "level = \"" .. args.level .. "\"\n")
|
||||
print("✔ Updated log level.")
|
||||
elseif args.tail then
|
||||
print(string.format("Tailing last %d lines...", args.tail))
|
||||
else
|
||||
print("Log module ready. Run 'mr log --help' for details.")
|
||||
end
|
||||
end
|
||||
8
commands/log/utils.lua
Normal file
8
commands/log/utils.lua
Normal file
@@ -0,0 +1,8 @@
|
||||
-- commands/log/utils.lua
|
||||
local M = {}
|
||||
|
||||
function M.format_level(level)
|
||||
return string.format("==> [LOG LEVEL: %s] <==", string.upper(level))
|
||||
end
|
||||
|
||||
return M
|
||||
18
mirror-cli/Cargo.toml
Normal file
18
mirror-cli/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "mirror-cli"
|
||||
version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
# 显式指定生成的二进制文件名
|
||||
[[bin]]
|
||||
name = "mr"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
mirror-core = { path = "../mirror-core" }
|
||||
clap = { version = "4.6.6", features = ["cargo", "color", "derive","string"] }
|
||||
clap_derive = { version = "4.6.4" }
|
||||
mlua = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
35
mirror-cli/src/app.rs
Normal file
35
mirror-cli/src/app.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use crate::cli;
|
||||
use crate::commands::{builtin, dynamic::MrCommand};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use mirror_core::{Layout, LuaRuntime};
|
||||
|
||||
pub fn run() -> Result<()> {
|
||||
let exe = std::env::current_exe().context("获取代理程序路径失败")?;
|
||||
let layout = Layout::from(&exe)?;
|
||||
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(()),
|
||||
}
|
||||
}
|
||||
62
mirror-cli/src/cli.rs
Normal file
62
mirror-cli/src/cli.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use crate::commands::dynamic::MrCommand;
|
||||
use clap::{Arg, ArgAction, Command};
|
||||
use mirror_core::LogLevel;
|
||||
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")
|
||||
.arg_required_else_help(true)
|
||||
.subcommand(log_subcommand());
|
||||
|
||||
for (name, sub_cmd) in dynamic_cmds.commands {
|
||||
if name == "log" {
|
||||
continue;
|
||||
}
|
||||
// let a =name.as_str()
|
||||
// 开启 "string" feature 后,String 可以直接作为 Command 的入参
|
||||
app = app.subcommand(
|
||||
Command::new(name)
|
||||
.about(sub_cmd.description)
|
||||
.allow_external_subcommands(true),
|
||||
);
|
||||
}
|
||||
app
|
||||
}
|
||||
/// 构建基础 CLI 骨架(内置 Rust 原生命令与参数规则)
|
||||
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("设置运行时日志级别")
|
||||
.value_parser(clap::value_parser!(LogLevel)),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("follow")
|
||||
.short('f')
|
||||
.long("follow")
|
||||
.help("持续跟踪日志输出 (tail -f)")
|
||||
.action(ArgAction::SetTrue),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn extract_raw_args(matches: &clap::ArgMatches) -> Vec<String> {
|
||||
matches
|
||||
.get_many::<OsString>("")
|
||||
.unwrap_or_default()
|
||||
.map(|s| s.to_string_lossy().into_owned())
|
||||
.collect()
|
||||
}
|
||||
35
mirror-cli/src/commands/builtin.rs
Normal file
35
mirror-cli/src/commands/builtin.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use anyhow::{Context, Result};
|
||||
use mirror_core::Layout;
|
||||
use std::fs;
|
||||
|
||||
// Rust 原生处理 log 命令逻辑
|
||||
pub(crate) fn log_handle(layout: &Layout, matches: &clap::ArgMatches) -> Result<()> {
|
||||
let log_ini_path = layout.base_dir.join("mirror-log.ini");
|
||||
|
||||
// 1. 处理设置日志等级
|
||||
if let Some(level) = matches.get_one::<String>("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)
|
||||
.with_context(|| format!("读取配置文件失败: {}", log_ini_path.display()))?;
|
||||
println!("当前 mirror-log.ini 配置:\n{current_ini}");
|
||||
} else {
|
||||
println!("未找到 mirror-log.ini,当前使用默认全局级别: info");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
139
mirror-cli/src/commands/dynamic.rs
Normal file
139
mirror-cli/src/commands/dynamic.rs
Normal file
@@ -0,0 +1,139 @@
|
||||
use crate::validators;
|
||||
use mirror_core::error::validation_error;
|
||||
use mlua::{FromLua, Lua, Value};
|
||||
use std::collections::HashMap;
|
||||
|
||||
const ALLOWED_KEYS: &[&str] = &["handler", "description"];
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct MrCommand {
|
||||
pub commands: HashMap<String, MrSubCommand>,
|
||||
}
|
||||
impl MrCommand {
|
||||
pub fn get(&self, name: &str) -> Option<&MrSubCommand> {
|
||||
self.commands.get(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromLua for MrCommand {
|
||||
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> {
|
||||
let root_tbl = match value {
|
||||
Value::Table(t) => t,
|
||||
other => {
|
||||
return Err(validation_error(format!(
|
||||
"commands 注册表的顶层配置必须是 Table,实际检测到: {}",
|
||||
other.type_name()
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let mut commands = HashMap::new();
|
||||
|
||||
for pair in root_tbl.pairs::<Value, Value>() {
|
||||
let (cmd_name, cmd_entry) = pair?;
|
||||
|
||||
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()
|
||||
)));
|
||||
}
|
||||
// 3. 直接交由子命令解析,外层负责补充错误上下文
|
||||
let cmd_def = MrSubCommand::from_lua(cmd_entry, lua).map_err(|err| {
|
||||
validation_error(format!("子命令 '{cmd_name}' 配置解析失败:\n{err}"))
|
||||
})?;
|
||||
|
||||
commands.insert(cmd_name, cmd_def);
|
||||
}
|
||||
Ok(Self { commands })
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct MrSubCommand {
|
||||
/// 命令处理函数:必须是 Lua Function
|
||||
pub(crate) module: String,
|
||||
/// 命令行说明:必须是普通文本字符串
|
||||
pub(crate) description: String,
|
||||
}
|
||||
|
||||
impl FromLua for MrSubCommand {
|
||||
fn from_lua(value: Value, _lua: &Lua) -> mlua::Result<Self> {
|
||||
let table = match value {
|
||||
Value::Table(t) => t,
|
||||
other => {
|
||||
return Err(validation_error(format!(
|
||||
"配置必须是 Table,实际检测到: {}",
|
||||
other.type_name()
|
||||
)));
|
||||
}
|
||||
};
|
||||
// validators::LuaValidator::parse_command_entry(&tbl)
|
||||
for pair in table.pairs::<Value, Value>() {
|
||||
let (key, _) = pair?;
|
||||
let key_str = validators::validate_command_name(&key)?;
|
||||
|
||||
if !ALLOWED_KEYS.contains(&key_str.as_str()) {
|
||||
return Err(validation_error(format!(
|
||||
"包含未知字段 '{key_str}'。\n\
|
||||
==> 合法字段仅支持: {}\n\
|
||||
==> 请检查是否存在拼写手误(例如把 handler 写成了 handle/handlr)",
|
||||
ALLOWED_KEYS.join(", ")
|
||||
)));
|
||||
}
|
||||
}
|
||||
let handler_val = table.get::<Value>("handler")?;
|
||||
// let handler_func = match handler_val {
|
||||
// Value::Function(f) => f,
|
||||
// Value::Boolean(true) => {
|
||||
// return Err(validation_error(
|
||||
// "子命令 handler 返回了布尔值 true。\n\
|
||||
// ==>排查提示:脚本已成功加载,但未返回函数。\n\
|
||||
// ==>请在末尾加上 'return function(args) ... end'。",
|
||||
// ));
|
||||
// }
|
||||
// Value::Nil => {
|
||||
// return Err(validation_error(
|
||||
// "子命令缺少必填字段 handler(或对应脚本未返回函数)",
|
||||
// ));
|
||||
// }
|
||||
// other => {
|
||||
// return Err(validation_error(format!(
|
||||
// "'handler' 必须是函数 (function),实际类型是: {}",
|
||||
// other.type_name()
|
||||
// )));
|
||||
// }
|
||||
// };
|
||||
let module = match handler_val {
|
||||
Value::Table(t) if t.get::<bool>("__is_lazy_command").unwrap_or(false) => {
|
||||
t.get::<String>("module")?
|
||||
}
|
||||
Value::String(s) => s.to_str()?.to_string(), // 同时兼容直接写字符串的情况
|
||||
other => {
|
||||
return Err(mlua::Error::runtime(format!(
|
||||
"handler 必须通过 command(\"xxx\") 声明,实际为 {}",
|
||||
other.type_name()
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
// 可选字段: args(缺失或 nil 时默认空列表;保留空字符串参数,与提权路径的 "" 语义一致)
|
||||
let description_val = table.get::<Value>("description")?;
|
||||
let description = match description_val {
|
||||
Value::Nil => String::new(),
|
||||
Value::String(s) => s.to_str()?.to_string(),
|
||||
other => {
|
||||
return Err(validation_error(format!(
|
||||
"必须是字符串文本,实际类型是 {}",
|
||||
other.type_name()
|
||||
)));
|
||||
}
|
||||
};
|
||||
// let handler = lua.create_registry_value(handler_func)?;
|
||||
Ok(Self {
|
||||
module,
|
||||
description,
|
||||
})
|
||||
}
|
||||
}
|
||||
2
mirror-cli/src/commands/mod.rs
Normal file
2
mirror-cli/src/commands/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub(crate) mod builtin;
|
||||
pub(crate) mod dynamic;
|
||||
14
mirror-cli/src/main.rs
Normal file
14
mirror-cli/src/main.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
mod app;
|
||||
mod cli;
|
||||
mod commands;
|
||||
mod validators;
|
||||
|
||||
use std::process::ExitCode;
|
||||
|
||||
fn main() -> ExitCode {
|
||||
if let Err(err) = app::run() {
|
||||
eprintln!("\x1b[31m[mr error]\x1b[0m {:#}", err);
|
||||
return ExitCode::FAILURE; // 返回退出码 1
|
||||
}
|
||||
ExitCode::SUCCESS // 返回退出码 0
|
||||
}
|
||||
41
mirror-cli/src/validators.rs
Normal file
41
mirror-cli/src/validators.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use mirror_core::error::validation_error;
|
||||
use mlua::Value;
|
||||
|
||||
pub fn validate_command_name(name: &Value) -> mlua::Result<String> {
|
||||
let name_str = match name {
|
||||
Value::String(s) => s
|
||||
.to_str()
|
||||
.map_err(|_| validation_error("子命令名称必须是合法的 UTF-8 字符串"))?
|
||||
.to_string(),
|
||||
other => {
|
||||
return Err(validation_error(format!(
|
||||
"命令行键名类型错误:期望 string,实际是 {}",
|
||||
other.type_name()
|
||||
)));
|
||||
}
|
||||
};
|
||||
let trimmed = name_str.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err(validation_error("子命令名称不能为空字符串"));
|
||||
}
|
||||
// 2. 禁止包含空格与不可见控制符(否则 shell 与 clap 无法正确定位)
|
||||
if name_str.contains(|c: char| c.is_whitespace()) {
|
||||
return Err(validation_error(format!(
|
||||
"子命令名称 [{name_str}] 非法:命令名不能包含空格或空白字符"
|
||||
)));
|
||||
}
|
||||
|
||||
if name_str.contains('=') {
|
||||
return Err(validation_error(format!(
|
||||
"子命令名称 [{}] 不能包含 '='",
|
||||
name_str
|
||||
)));
|
||||
}
|
||||
if name_str.contains('\0') {
|
||||
return Err(validation_error(format!(
|
||||
"子命令名称 [{}] 不能包含 NUL 字符",
|
||||
name_str
|
||||
)));
|
||||
}
|
||||
Ok(name_str)
|
||||
}
|
||||
16
mirror-core/Cargo.toml
Normal file
16
mirror-core/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "mirror-core"
|
||||
version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
mlua = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
tinyjson = { workspace = true }
|
||||
windows-sys = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
# 日志
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
tracing-appender = { workspace = true }
|
||||
@@ -3,13 +3,12 @@ mod layout;
|
||||
mod logger;
|
||||
mod mirror;
|
||||
mod runtime;
|
||||
pub mod sys;
|
||||
mod utils;
|
||||
mod validators;
|
||||
pub mod validators;
|
||||
|
||||
pub use layout::Layout;
|
||||
pub use mirror::Mirror;
|
||||
pub use runtime::LuaRuntime;
|
||||
|
||||
pub use logger::{Logger, init_logging_from};
|
||||
pub use logger::{init_logging_from, Logger,LogLevel};
|
||||
pub use utils::{lua_string_2_os_string, normalize_path_for_lua, parse_tokens};
|
||||
@@ -7,6 +7,7 @@ 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 {
|
||||
@@ -18,6 +19,13 @@ pub enum LogLevel {
|
||||
Debug,
|
||||
Trace,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for LogLevel {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl LogLevel {
|
||||
/// 是否需要激活写盘排查模式(Info 及以上更详细的级别)
|
||||
pub fn is_verbose(&self) -> bool {
|
||||
@@ -36,7 +44,7 @@ impl LogLevel {
|
||||
}
|
||||
}
|
||||
impl FromStr for LogLevel {
|
||||
type Err = ();
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_ascii_lowercase().as_str() {
|
||||
@@ -46,7 +54,9 @@ impl FromStr for LogLevel {
|
||||
"info" => Ok(LogLevel::Info),
|
||||
"debug" => Ok(LogLevel::Debug),
|
||||
"trace" => Ok(LogLevel::Trace),
|
||||
_ => Err(()),
|
||||
_ => Err(format!(
|
||||
"无效的日志级别 '{s}',可选值为: off, error, warn, info, debug, trace"
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,7 +111,7 @@ impl Logger {
|
||||
pub fn init_logging_from(layout: &Layout) -> Option<WorkerGuard> {
|
||||
// 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_path = layout.base_dir.join("../../mirror.ini");
|
||||
|
||||
// 读取配置文件(如不存在或解析失败,降级回退到默认设置)
|
||||
let config = if config_path.exists() {
|
||||
@@ -114,7 +124,7 @@ pub fn init_logging_from(layout: &Layout) -> Option<WorkerGuard> {
|
||||
Logger::default()
|
||||
};
|
||||
|
||||
// 1. 优先读取环境变量 SHIM_LOG,解析失败或未设置则退回到 config.level
|
||||
// 1. 优先读取环境变量 MIRROR_LOG_LEVEL,解析失败或未设置则退回到 config.level
|
||||
let effective_level = std::env::var("MIRROR_LOG_LEVEL")
|
||||
.ok()
|
||||
.and_then(|val| LogLevel::from_str(&val).ok())
|
||||
@@ -20,7 +20,7 @@ pub struct Mirror {
|
||||
impl Mirror {
|
||||
pub fn load(layout: &Layout) -> Result<Self> {
|
||||
// 策略 1: 尝试加载全局配置文件 mirror.lua
|
||||
let global_config = layout.base_dir.join("mirror.lua");
|
||||
let global_config = layout.base_dir.join("../../mirror.lua");
|
||||
|
||||
if !global_config.is_file() {
|
||||
bail!("主配置文件不存在: {}", global_config.display());
|
||||
@@ -36,6 +36,7 @@ impl LuaRuntime {
|
||||
Self::register_get_env(&lua)?;
|
||||
// 3. 初始化并配置安全/容错的 require 机制
|
||||
Self::setup_require(&lua, &base_dir)?;
|
||||
|
||||
Ok(Self { lua })
|
||||
}
|
||||
|
||||
@@ -61,8 +62,44 @@ impl LuaRuntime {
|
||||
.set_name(&chunk_name)
|
||||
.eval::<T>()
|
||||
.map_err(syntax_error)
|
||||
.with_context(|| format!("执行 Lua 配置文件失败: {}", path.display()))
|
||||
.with_context(|| format!("执行 {} 配置文件失败: {}", chunk_name,path.display()))
|
||||
}
|
||||
|
||||
/// 传入注册表的 handler key 与命令行原始参数,内部完成调用
|
||||
pub fn call_handler(
|
||||
&self,
|
||||
handler_key: &mlua::RegistryKey,
|
||||
args: Vec<String>,
|
||||
) -> Result<()> {
|
||||
let handler: mlua::Function = self.lua.registry_value(handler_key)?;
|
||||
let lua_args = self.lua.create_sequence_from(args)?;
|
||||
|
||||
if let Err(err) = handler.call::<()>(lua_args) {
|
||||
let msg = err.to_string();
|
||||
// 内部消化类似 argparse 的帮助退出信号
|
||||
if msg.contains("__ARGPARSE_HELP__") {
|
||||
return Ok(());
|
||||
}
|
||||
anyhow::bail!("{msg}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn require_and_run(&self, module_name: &str, raw_args: Vec<String>) -> Result<()> {
|
||||
let require: mlua::Function = self.lua.globals().get("require")?;
|
||||
let handler_val: mlua::Value = require.call(module_name)?;
|
||||
|
||||
let func = match handler_val {
|
||||
mlua::Value::Function(f) => f,
|
||||
other => anyhow::bail!("模块 '{module_name}' 必须返回一个函数,实际为 {}", other.type_name()),
|
||||
};
|
||||
|
||||
let lua_args = self.lua.create_sequence_from(raw_args)?;
|
||||
func.call::<()>(lua_args)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
}
|
||||
impl LuaRuntime {
|
||||
/// 注入全局锚点变量
|
||||
@@ -118,16 +155,22 @@ impl LuaRuntime {
|
||||
let globals = lua.globals();
|
||||
|
||||
// 1. 安全加固并拓展 package 搜索路径
|
||||
if let Ok(package) = globals.get::<Table>("package") {
|
||||
let package = globals.get::<Table>("package")?;
|
||||
|
||||
let _ = package.set("cpath", "");
|
||||
let _ = package.set("loadlib", Value::Nil);
|
||||
|
||||
if let Ok(path) = package.get::<String>("path") {
|
||||
let new_path = format!("{};{}/?.lua;{}/?/init.lua;", path, base_dir, base_dir);
|
||||
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 查找路径");
|
||||
let _ = package.set("path", new_path);
|
||||
}
|
||||
}
|
||||
package.set("path", new_path)?;
|
||||
|
||||
// 2. 获取原生 require 并通过闭包直接持有(无需向全局表注入备份变量)
|
||||
let original_require: mlua::Function = globals
|
||||
24
mirror-shim/Cargo.toml
Normal file
24
mirror-shim/Cargo.toml
Normal file
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "mirror-shim"
|
||||
version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
mirror-core = { path = "../mirror-core" }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
|
||||
windows-sys = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
tracing-appender = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
args = []
|
||||
|
||||
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
panic = "abort"
|
||||
@@ -1,9 +1,11 @@
|
||||
use mirror::{Layout, Mirror,init_logging_from};
|
||||
use mirror::sys::*;
|
||||
use mirror_core::{init_logging_from, Layout, Mirror};
|
||||
use std::ffi::OsString;
|
||||
use std::{env, process::exit};
|
||||
use anyhow::Context;
|
||||
use tracing::{debug, error, warn};
|
||||
use tracing::{debug, error};
|
||||
use crate::sys::*;
|
||||
|
||||
pub mod sys;
|
||||
|
||||
fn main() {
|
||||
let current_exe = match env::current_exe().context("获取代理程序路径失败"){
|
||||
@@ -10,6 +10,7 @@ return {
|
||||
-- 1. 标准相对路径 + 正斜杠拼接 (最推荐,绿色便携)
|
||||
---------------------------------------------------
|
||||
["numa"] = {
|
||||
home="",
|
||||
target = base_dir .. "/tools/numa/numa.exe",
|
||||
-- 追加参数
|
||||
args = { "--help" },
|
||||
|
||||
1527
vendor/argparse.lua
vendored
Normal file
1527
vendor/argparse.lua
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user