support config
This commit is contained in:
14
src-tauri/Cargo.lock
generated
14
src-tauri/Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +86,12 @@
|
||||
<span class="wait-text">等待中</span>
|
||||
</div>
|
||||
|
||||
<!-- 配置中状态 -->
|
||||
<div v-else-if="software.status === 'configuring'" class="status-configuring">
|
||||
<div class="mini-spinner"></div>
|
||||
<span class="config-text">正在配置...</span>
|
||||
</div>
|
||||
|
||||
<!-- 安装中状态:显示进度环和百分比 -->
|
||||
<div v-else-if="software.status === 'installing'" class="progress-status">
|
||||
<div class="progress-ring-container">
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user