support config
This commit is contained in:
@@ -8,8 +8,10 @@ use std::path::PathBuf;
|
||||
use tokio::sync::mpsc;
|
||||
use tauri::{AppHandle, Manager, State, Emitter};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use winget::{Software, list_installed_software, list_updates, ensure_winget_dependencies};
|
||||
use winget::{Software, list_installed_software, list_updates, ensure_winget_dependencies, PostInstallStep};
|
||||
use regex::Regex;
|
||||
use winreg::RegKey;
|
||||
use winreg::enums::*;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct AppSettings {
|
||||
@@ -180,6 +182,118 @@ fn get_logs_history() -> Vec<LogPayload> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn expand_win_path(path: &str) -> PathBuf {
|
||||
let mut expanded = path.to_string();
|
||||
let env_vars = [
|
||||
"AppData", "LocalAppData", "ProgramData", "SystemRoot", "SystemDrive", "TEMP", "USERPROFILE"
|
||||
];
|
||||
for var in env_vars {
|
||||
let placeholder = format!("%{}%", var);
|
||||
if expanded.contains(&placeholder) {
|
||||
if let Ok(val) = std::env::var(var) {
|
||||
expanded = expanded.replace(&placeholder, &val);
|
||||
}
|
||||
}
|
||||
}
|
||||
PathBuf::from(expanded)
|
||||
}
|
||||
|
||||
async fn execute_post_install(handle: &AppHandle, log_id: &str, steps: Vec<PostInstallStep>) -> Result<(), String> {
|
||||
let steps_len = steps.len();
|
||||
for (i, step) in steps.into_iter().enumerate() {
|
||||
let step_prefix = format!("Step {}/{}: ", i + 1, steps_len);
|
||||
match step {
|
||||
PostInstallStep::RegistryBatch { root, base_path, values } => {
|
||||
emit_log(handle, log_id, "Registry Update", &format!("{}Applying batch registry settings to {}...", step_prefix, base_path), "info");
|
||||
let hive = match root.as_str() {
|
||||
"HKCU" => RegKey::predef(HKEY_CURRENT_USER),
|
||||
"HKLM" => RegKey::predef(HKEY_LOCAL_MACHINE),
|
||||
_ => {
|
||||
emit_log(handle, log_id, "Registry Error", &format!("Unknown root hive: {}", root), "error");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
match hive.create_subkey(&base_path) {
|
||||
Ok((key, _)) => {
|
||||
for (name, val) in values {
|
||||
let res = match val.v_type.as_str() {
|
||||
"String" => key.set_value(&name, &val.data.as_str().unwrap_or_default()),
|
||||
"Dword" => key.set_value(&name, &(val.data.as_u64().unwrap_or(0) as u32)),
|
||||
"Qword" => key.set_value(&name, &(val.data.as_u64().unwrap_or(0))),
|
||||
"MultiString" => {
|
||||
let strings: Vec<String> = val.data.as_array()
|
||||
.map(|a| a.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
|
||||
.unwrap_or_default();
|
||||
key.set_value(&name, &strings)
|
||||
},
|
||||
"ExpandString" => {
|
||||
key.set_value(&name, &val.data.as_str().unwrap_or_default())
|
||||
},
|
||||
_ => Err(std::io::Error::new(std::io::ErrorKind::Other, "Unsupported type")),
|
||||
};
|
||||
if let Err(e) = res {
|
||||
emit_log(handle, log_id, "Registry Error", &format!("Failed to set {}: {}", name, e), "error");
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
emit_log(handle, log_id, "Registry Error", &format!("Failed to create/open key {}: {}", base_path, e), "error");
|
||||
}
|
||||
}
|
||||
},
|
||||
PostInstallStep::FileReplace { url, target } => {
|
||||
let target_path = expand_win_path(&target);
|
||||
emit_log(handle, log_id, "File Replace", &format!("{}Downloading configuration file to {:?}...", step_prefix, target_path), "info");
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
match client.get(&url).timeout(std::time::Duration::from_secs(30)).send().await {
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
if let Ok(bytes) = resp.bytes().await {
|
||||
if let Some(parent) = target_path.parent() {
|
||||
let _ = fs::create_dir_all(parent);
|
||||
}
|
||||
if let Err(e) = fs::write(&target_path, bytes) {
|
||||
emit_log(handle, log_id, "File Error", &format!("Failed to write to {:?}: {}", target_path, e), "error");
|
||||
} else {
|
||||
emit_log(handle, log_id, "Success", "File replaced successfully.", "success");
|
||||
}
|
||||
}
|
||||
},
|
||||
Ok(resp) => {
|
||||
emit_log(handle, log_id, "Download Error", &format!("HTTP Status: {}", resp.status()), "error");
|
||||
},
|
||||
Err(e) => {
|
||||
emit_log(handle, log_id, "Download Error", &e.to_string(), "error");
|
||||
}
|
||||
}
|
||||
},
|
||||
PostInstallStep::Command { run } => {
|
||||
emit_log(handle, log_id, "Command Execution", &format!("{}Executing: {}", step_prefix, run), "info");
|
||||
let output = Command::new("cmd")
|
||||
.args(["/C", &run])
|
||||
.creation_flags(0x08000000)
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(out) => {
|
||||
if !out.status.success() {
|
||||
let err = String::from_utf8_lossy(&out.stderr);
|
||||
emit_log(handle, log_id, "Command Failed", &err, "error");
|
||||
} else {
|
||||
emit_log(handle, log_id, "Success", "Command executed successfully.", "success");
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
emit_log(handle, log_id, "Execution Error", &e.to_string(), "error");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
struct InstallProgress {
|
||||
id: String,
|
||||
@@ -207,7 +321,6 @@ pub fn run() {
|
||||
|
||||
let log_id = format!("install-{}", task_id);
|
||||
|
||||
// 1. 发送正在安装状态
|
||||
let _ = handle.emit("install-status", InstallProgress {
|
||||
id: task_id.clone(),
|
||||
status: "installing".to_string(),
|
||||
@@ -291,7 +404,6 @@ pub fn run() {
|
||||
Ok(mut child_proc) => {
|
||||
if let Some(stdout) = child_proc.stdout.take() {
|
||||
let reader = BufReader::new(stdout);
|
||||
// 使用 split(b'\r') 是为了捕捉 PowerShell 中的动态进度行
|
||||
for line_res in reader.split(b'\r') {
|
||||
if let Ok(line_bytes) = line_res {
|
||||
let line_str = String::from_utf8_lossy(&line_bytes).to_string();
|
||||
@@ -328,7 +440,48 @@ pub fn run() {
|
||||
}
|
||||
}
|
||||
let exit_status = child_proc.wait().map(|s| s.success()).unwrap_or(false);
|
||||
if exit_status { "success" } else { "error" }
|
||||
let status_result = if exit_status { "success" } else { "error" };
|
||||
|
||||
if status_result == "success" {
|
||||
let essentials = get_essentials(handle.clone());
|
||||
let software_info = essentials.and_then(|repo| {
|
||||
repo.essentials.into_iter().find(|s| s.id == task_id)
|
||||
});
|
||||
|
||||
if let Some(sw) = software_info {
|
||||
let mut final_steps = None;
|
||||
if let Some(steps) = sw.post_install {
|
||||
if !steps.is_empty() {
|
||||
final_steps = Some(steps);
|
||||
}
|
||||
} else if let Some(url) = sw.post_install_url {
|
||||
emit_log(&handle, &log_id, "Post-Install", "Local config not found, fetching remote config...", "info");
|
||||
let client = reqwest::Client::new();
|
||||
if let Ok(resp) = client.get(&url).timeout(std::time::Duration::from_secs(10)).send().await {
|
||||
if resp.status().is_success() {
|
||||
if let Ok(steps) = resp.json::<Vec<PostInstallStep>>().await {
|
||||
final_steps = Some(steps);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(steps) = final_steps {
|
||||
let _ = handle.emit("install-status", InstallProgress {
|
||||
id: task_id.clone(),
|
||||
status: "configuring".to_string(),
|
||||
progress: 1.0,
|
||||
});
|
||||
emit_log(&handle, &log_id, "Post-Install", "Starting post-installation configuration...", "info");
|
||||
if let Err(e) = execute_post_install(&handle, &log_id, steps).await {
|
||||
emit_log(&handle, &log_id, "Post-Install Error", &e, "error");
|
||||
} else {
|
||||
emit_log(&handle, &log_id, "Post-Install", "Post-installation configuration completed.", "success");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
status_result
|
||||
},
|
||||
Err(e) => {
|
||||
emit_log(&h, &log_id, "Fatal Error", &e.to_string(), "error");
|
||||
@@ -336,16 +489,13 @@ pub fn run() {
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 发送最终完成/失败状态
|
||||
let _ = handle.emit("install-status", InstallProgress {
|
||||
id: task_id.clone(),
|
||||
status: status_result.to_string(),
|
||||
progress: 1.0,
|
||||
});
|
||||
|
||||
emit_log(&handle, &log_id, "Result", &format!("Execution finished: {}", status_result), if status_result == "success" { "success" } else { "error" });
|
||||
|
||||
// 3. 清理临时清单文件
|
||||
if let Some(path) = temp_manifest_path {
|
||||
let _ = fs::remove_file(path);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,36 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::process::Command;
|
||||
use std::os::windows::process::CommandExt;
|
||||
use std::collections::HashMap;
|
||||
use tauri::AppHandle;
|
||||
use crate::emit_log;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct RegistryValue {
|
||||
pub v_type: String, // "String", "Dword", "Qword", "MultiString", "ExpandString"
|
||||
pub data: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum PostInstallStep {
|
||||
#[serde(rename = "registry_batch")]
|
||||
RegistryBatch {
|
||||
root: String, // "HKCU", "HKLM"
|
||||
base_path: String,
|
||||
values: HashMap<String, RegistryValue>,
|
||||
},
|
||||
#[serde(rename = "file_replace")]
|
||||
FileReplace {
|
||||
url: String,
|
||||
target: String, // 支持 %AppData% 等环境变量占位符
|
||||
},
|
||||
#[serde(rename = "command")]
|
||||
Command {
|
||||
run: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Software {
|
||||
pub id: String,
|
||||
@@ -13,12 +40,14 @@ pub struct Software {
|
||||
pub available_version: Option<String>,
|
||||
pub icon_url: Option<String>,
|
||||
#[serde(default = "default_status")]
|
||||
pub status: String, // "idle", "pending", "installing", "success", "error"
|
||||
pub status: String, // "idle", "pending", "installing", "configuring", "success", "error"
|
||||
#[serde(default = "default_progress")]
|
||||
pub progress: f32,
|
||||
#[serde(default = "default_false")]
|
||||
pub use_manifest: bool,
|
||||
pub manifest_url: Option<String>,
|
||||
pub post_install: Option<Vec<PostInstallStep>>,
|
||||
pub post_install_url: Option<String>,
|
||||
}
|
||||
|
||||
fn default_status() -> String { "idle".to_string() }
|
||||
@@ -283,5 +312,7 @@ fn map_package(p: WingetPackage) -> Software {
|
||||
progress: 0.0,
|
||||
use_manifest: false,
|
||||
manifest_url: None,
|
||||
post_install: None,
|
||||
post_install_url: None,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user