Files
kami/license-system-launcher/src/bin/launcher_gui.rs
2026-01-04 23:00:21 +08:00

243 lines
8.5 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::env;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use eframe::egui;
const MAGIC: &[u8] = b"LSWRAP1";
fn main() -> eframe::Result<()> {
let options = eframe::NativeOptions::default();
eframe::run_native(
"授权打包器",
options,
Box::new(|_| Box::new(LauncherGui::new())),
)
}
struct LauncherGui {
api_base: String,
integration_code: String,
input_path: String,
output_path: String,
extract_to_self: bool,
status: String,
}
impl LauncherGui {
fn new() -> Self {
Self {
api_base: String::new(),
integration_code: String::new(),
input_path: String::new(),
output_path: String::new(),
extract_to_self: false,
status: String::new(),
}
}
fn pack(&mut self) -> Result<(), String> {
let api_base = normalize_api_base(&self.api_base)?;
let (project_id, project_key) = decode_integration_code(&self.integration_code)?;
let input = PathBuf::from(self.input_path.trim());
if !input.exists() {
return Err("未选择可执行文件".to_string());
}
let output = if self.output_path.trim().is_empty() {
derive_output_path(&input)?
} else {
PathBuf::from(self.output_path.trim())
};
if input == output {
return Err("输出文件不能与输入文件相同".to_string());
}
let exe_dir = env::current_exe()
.map_err(|e| format!("无法定位程序目录: {e}"))?
.parent()
.unwrap_or(Path::new("."))
.to_path_buf();
let stub_path = exe_dir.join("launcher_stub.exe");
if !stub_path.exists() {
return Err("未找到 launcher_stub.exe请放在同目录".to_string());
}
let payload_name = input
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("payload.exe")
.to_string();
let extract_to = if self.extract_to_self { "self" } else { "temp" };
let config = serde_json::json!({
"api_base": api_base,
"project_id": project_id,
"project_secret": project_key,
"license_file": "license.key",
"request_timeout_sec": 10,
"heartbeat_retries": 2,
"heartbeat_retry_delay_sec": 5,
"extract_to": extract_to,
"keep_payload": false,
"payload_name": payload_name
});
let config_bytes = serde_json::to_vec(&config)
.map_err(|e| format!("配置序列化失败: {e}"))?;
pack_files(&stub_path, &input, &output, &config_bytes)?;
self.output_path = output.to_string_lossy().to_string();
Ok(())
}
}
impl eframe::App for LauncherGui {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
handle_drop(ctx, self);
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("授权打包器");
ui.add_space(8.0);
ui.label("授权系统地址");
ui.add(egui::TextEdit::singleline(&mut self.api_base).hint_text("http://118.145.218.2:39256"));
ui.add_space(6.0);
ui.label("项目对接码");
ui.add(egui::TextEdit::singleline(&mut self.integration_code).hint_text("LSC1.xxxxx"));
ui.add_space(6.0);
ui.label("选择要打包的 EXE");
ui.horizontal(|ui| {
ui.add(egui::TextEdit::singleline(&mut self.input_path).desired_width(360.0));
if ui.button("浏览").clicked() {
if let Some(file) = rfd::FileDialog::new().add_filter("EXE", &["exe"]).pick_file() {
self.input_path = file.to_string_lossy().to_string();
if self.output_path.trim().is_empty() {
if let Ok(path) = derive_output_path(&file) {
self.output_path = path.to_string_lossy().to_string();
}
}
}
}
});
ui.add_space(6.0);
ui.label("输出文件");
ui.horizontal(|ui| {
ui.add(egui::TextEdit::singleline(&mut self.output_path).desired_width(360.0));
if ui.button("另存为").clicked() {
if let Some(file) = rfd::FileDialog::new().add_filter("EXE", &["exe"]).set_file_name("packed.exe").save_file() {
self.output_path = file.to_string_lossy().to_string();
}
}
});
ui.add_space(6.0);
ui.checkbox(&mut self.extract_to_self, "解压到程序目录(需要本地资源时勾选)");
ui.add_space(10.0);
if ui.button("开始打包").clicked() {
match self.pack() {
Ok(_) => self.status = "打包完成".to_string(),
Err(err) => self.status = format!("打包失败: {err}"),
}
}
if !self.status.is_empty() {
ui.add_space(8.0);
ui.label(&self.status);
}
});
}
}
fn handle_drop(ctx: &egui::Context, app: &mut LauncherGui) {
let dropped = ctx.input(|i| i.raw.dropped_files.clone());
for file in dropped {
if let Some(path) = file.path {
if path.extension().and_then(|s| s.to_str()).map(|s| s.eq_ignore_ascii_case("exe")).unwrap_or(false) {
app.input_path = path.to_string_lossy().to_string();
if app.output_path.trim().is_empty() {
if let Ok(out) = derive_output_path(&path) {
app.output_path = out.to_string_lossy().to_string();
}
}
break;
}
}
}
}
fn pack_files(stub_path: &Path, payload_path: &Path, output_path: &Path, config: &[u8]) -> Result<(), String> {
let stub = fs::read(stub_path).map_err(|e| format!("读取 stub 失败: {e}"))?;
let payload = fs::read(payload_path).map_err(|e| format!("读取 EXE 失败: {e}"))?;
let mut out = fs::File::create(output_path).map_err(|e| format!("创建输出失败: {e}"))?;
out.write_all(&stub).map_err(|e| format!("写入 stub 失败: {e}"))?;
out.write_all(&payload).map_err(|e| format!("写入 EXE 失败: {e}"))?;
out.write_all(config).map_err(|e| format!("写入配置失败: {e}"))?;
out.write_all(MAGIC).map_err(|e| format!("写入标识失败: {e}"))?;
out.write_all(&(payload.len() as u64).to_le_bytes())
.map_err(|e| format!("写入长度失败: {e}"))?;
out.write_all(&(config.len() as u64).to_le_bytes())
.map_err(|e| format!("写入长度失败: {e}"))?;
out.flush().map_err(|e| format!("写入失败: {e}"))?;
Ok(())
}
fn normalize_api_base(value: &str) -> Result<String, String> {
let trimmed = value.trim().trim_end_matches('/');
if trimmed.is_empty() {
return Err("请输入授权系统地址".to_string());
}
Ok(trimmed.to_string())
}
fn derive_output_path(input: &Path) -> Result<PathBuf, String> {
let parent = input.parent().unwrap_or(Path::new("."));
let stem = input
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("app");
let ext = input
.extension()
.and_then(|s| s.to_str())
.unwrap_or("exe");
let name = format!("{stem}_packed.{ext}");
Ok(parent.join(name))
}
fn decode_integration_code(code: &str) -> Result<(String, String), String> {
let trimmed = code.trim();
if trimmed.is_empty() {
return Err("请输入对接码".to_string());
}
if trimmed.contains('|') {
return split_code(trimmed);
}
let raw = trimmed.strip_prefix("LSC1.").unwrap_or(trimmed);
let decoded = STANDARD.decode(raw).map_err(|_| "对接码格式错误".to_string())?;
let decoded_str = String::from_utf8(decoded).map_err(|_| "对接码解析失败".to_string())?;
split_code(&decoded_str)
}
fn split_code(value: &str) -> Result<(String, String), String> {
let mut parts = value.split('|');
let project_id = parts.next().unwrap_or("").trim();
let project_key = parts.next().unwrap_or("").trim();
if project_id.is_empty() || project_key.is_empty() {
return Err("对接码内容不完整".to_string());
}
Ok((project_id.to_string(), project_key.to_string()))
}