From 04e4a510e5d5a7672dceb49f741720f545fd904d Mon Sep 17 00:00:00 2001 From: Julian Freeman Date: Sat, 4 Apr 2026 13:22:40 -0400 Subject: [PATCH] support config --- src-tauri/Cargo.lock | 14 ++- src-tauri/Cargo.toml | 1 + src-tauri/src/lib.rs | 164 ++++++++++++++++++++++++++++++-- src-tauri/src/winget.rs | 33 ++++++- src/components/SoftwareCard.vue | 30 ++++++ src/store/software.ts | 4 +- 6 files changed, 236 insertions(+), 10 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 1f4cf71..1da46d9 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -859,7 +859,7 @@ dependencies = [ "rustc_version", "toml 0.9.12+spec-1.1.0", "vswhom", - "winreg", + "winreg 0.55.0", ] [[package]] @@ -5117,6 +5117,7 @@ dependencies = [ "tauri-build", "tauri-plugin-opener", "tokio", + "winreg 0.56.0", ] [[package]] @@ -5592,6 +5593,17 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "winreg" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" +dependencies = [ + "cfg-if", + "serde", + "windows-sys 0.61.2", +] + [[package]] name = "wit-bindgen" version = "0.51.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 3f1675b..43e6b49 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -26,4 +26,5 @@ tokio = { version = "1.50.0", features = ["full"] } chrono = "0.4.44" regex = "1.12.3" reqwest = { version = "0.12", features = ["json", "rustls-tls"] } +winreg = { version = "0.56.0", features = ["serde"] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a4ad1dd..ce604ca 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -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 { 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) -> 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 = 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::>().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); } diff --git a/src-tauri/src/winget.rs b/src-tauri/src/winget.rs index 89123b2..f916f06 100644 --- a/src-tauri/src/winget.rs +++ b/src-tauri/src/winget.rs @@ -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, + }, + #[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, pub icon_url: Option, #[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, + pub post_install: Option>, + pub post_install_url: Option, } 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, } } diff --git a/src/components/SoftwareCard.vue b/src/components/SoftwareCard.vue index 8a00b25..2cbc492 100644 --- a/src/components/SoftwareCard.vue +++ b/src/components/SoftwareCard.vue @@ -86,6 +86,12 @@ 等待中 + +
+
+ 正在配置... +
+
@@ -373,6 +379,30 @@ const handleCardClick = () => { color: #AEAEB2; } +.status-configuring { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + width: 90px; + justify-content: center; + color: var(--primary-color); +} + +.mini-spinner { + width: 14px; + height: 14px; + border: 2px solid rgba(0, 122, 255, 0.1); + border-top-color: var(--primary-color); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +.config-text { + font-size: 11px; + font-weight: 600; +} + .wait-text { font-size: 12px; font-weight: 600; diff --git a/src/store/software.ts b/src/store/software.ts index 0135a1a..20f4abc 100644 --- a/src/store/software.ts +++ b/src/store/software.ts @@ -283,9 +283,11 @@ export const useSoftwareStore = defineStore('software', { listen('install-status', (event: any) => { const { id, status, progress } = event.payload const task = this.activeTasks[id]; + + // 更新任务状态 this.activeTasks[id] = { status, progress, targetVersion: task?.targetVersion }; - // 当任务达到终态(成功或失败)时 + // 当任务达到终态(成功或失败)时。注意:'configuring' 不是终态。 if (status === 'success' || status === 'error') { if (status === 'success') { this.lastFetched = 0;