Files
win-softmgr/src-tauri/src/lib.rs
Julian Freeman c230847cc0 support del reg
2026-04-04 15:54:40 -04:00

524 lines
23 KiB
Rust
Raw 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.
pub mod winget;
use std::fs;
use std::process::{Command, Stdio};
use std::os::windows::process::CommandExt;
use std::io::{BufRead, BufReader};
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, PostInstallStep};
use regex::Regex;
use winreg::RegKey;
use winreg::enums::*;
#[derive(Clone, Serialize, Deserialize)]
pub struct AppSettings {
pub repo_url: String,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct EssentialsRepo {
pub version: String,
pub essentials: Vec<Software>,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct InstallTask {
pub id: String,
pub version: Option<String>,
#[serde(default)]
pub use_manifest: bool,
pub manifest_url: Option<String>,
}
impl Default for AppSettings {
fn default() -> Self {
Self {
repo_url: "https://karlblue.github.io/winget-repo".to_string(),
}
}
}
struct AppState {
install_tx: mpsc::Sender<InstallTask>,
}
#[derive(Clone, Serialize)]
pub struct LogPayload {
pub id: String,
pub timestamp: String,
pub command: String,
pub output: String,
pub status: String,
}
pub fn emit_log(handle: &AppHandle, id: &str, command: &str, output: &str, status: &str) {
let now = chrono::Local::now().format("%H:%M:%S").to_string();
let _ = handle.emit("log-event", LogPayload {
id: id.to_string(),
timestamp: now,
command: command.to_string(),
output: output.to_string(),
status: status.to_string(),
});
}
fn get_settings_path(app: &AppHandle) -> PathBuf {
let app_data_dir = app.path().app_data_dir().unwrap_or_default();
if !app_data_dir.exists() {
let _ = fs::create_dir_all(&app_data_dir);
}
app_data_dir.join("settings.json")
}
fn get_essentials_path(app: &AppHandle) -> PathBuf {
let app_data_dir = app.path().app_data_dir().unwrap_or_default();
if !app_data_dir.exists() {
let _ = fs::create_dir_all(&app_data_dir);
}
app_data_dir.join("setup-essentials.json")
}
#[tauri::command]
fn get_settings(app: AppHandle) -> AppSettings {
let path = get_settings_path(&app);
if !path.exists() {
let default_settings = AppSettings::default();
let _ = fs::write(&path, serde_json::to_string_pretty(&default_settings).unwrap());
return default_settings;
}
let content = fs::read_to_string(path).unwrap_or_default();
serde_json::from_str(&content).unwrap_or_default()
}
#[tauri::command]
fn save_settings(app: AppHandle, settings: AppSettings) -> Result<(), String> {
let path = get_settings_path(&app);
let content = serde_json::to_string_pretty(&settings).map_err(|e| e.to_string())?;
fs::write(path, content).map_err(|e| e.to_string())
}
#[tauri::command]
async fn sync_essentials(app: AppHandle) -> Result<bool, String> {
let settings = get_settings(app.clone());
let url = format!("{}/setup-essentials.json", settings.repo_url.trim_end_matches('/'));
emit_log(&app, "sync-essentials", "Syncing Essentials", &format!("Downloading from {}...", url), "info");
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()
.map_err(|e| e.to_string())?;
match client.get(&url).send().await {
Ok(response) => {
if response.status().is_success() {
let content = response.text().await.map_err(|e| e.to_string())?;
// 验证 JSON 格式(新格式:{ version: string, essentials: Vec<Software> }
let validation: Result<EssentialsRepo, _> = serde_json::from_str(&content);
if validation.is_ok() {
let path = get_essentials_path(&app);
fs::write(path, content).map_err(|e| e.to_string())?;
emit_log(&app, "sync-essentials", "Result", "Essentials list updated successfully.", "success");
Ok(true)
} else {
emit_log(&app, "sync-essentials", "Error", "Invalid JSON format from repository. Expected { version, essentials }.", "error");
Err("Invalid JSON format".to_string())
}
} else {
let err_msg = format!("HTTP Error: {}", response.status());
emit_log(&app, "sync-essentials", "Error", &err_msg, "error");
Err(err_msg)
}
}
Err(e) => {
emit_log(&app, "sync-essentials", "Skipped", &format!("Network issue: {}. Using local cache.", e), "info");
Ok(false)
}
}
}
#[tauri::command]
async fn initialize_app(app: AppHandle) -> Result<bool, String> {
let app_clone = app.clone();
tokio::task::spawn_blocking(move || {
ensure_winget_dependencies(&app_clone).map(|_| true)
}).await.unwrap_or(Err("Initialization Task Panicked".to_string()))
}
#[tauri::command]
fn get_essentials(app: AppHandle) -> Option<EssentialsRepo> {
let file_path = get_essentials_path(&app);
if !file_path.exists() {
return None;
}
let content = fs::read_to_string(file_path).unwrap_or_default();
serde_json::from_str(&content).ok()
}
#[tauri::command]
async fn get_installed_software(app: AppHandle) -> Vec<Software> {
tokio::task::spawn_blocking(move || list_installed_software(&app)).await.unwrap_or_default()
}
#[tauri::command]
async fn get_updates(app: AppHandle) -> Vec<Software> {
tokio::task::spawn_blocking(move || list_updates(&app)).await.unwrap_or_default()
}
#[tauri::command]
async fn install_software(
task: InstallTask,
state: State<'_, AppState>
) -> Result<(), String> {
state.install_tx.send(task).await.map_err(|e| e.to_string())
}
#[tauri::command]
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())
},
"Delete" => {
key.delete_value(&name)
},
_ => 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 apply {}: {}", 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,
status: String,
progress: f32,
}
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.setup(move |app| {
let handle = app.handle().clone();
let (tx, mut rx) = mpsc::channel::<InstallTask>(100);
app.manage(AppState { install_tx: tx });
tauri::async_runtime::spawn(async move {
let perc_re = Regex::new(r"(\d+)\s*%").unwrap();
let size_re = Regex::new(r"([\d\.]+)\s*[a-zA-Z]+\s*/\s*([\d\.]+)\s*[a-zA-Z]+").unwrap();
while let Some(task) = rx.recv().await {
let task_id = task.id.clone();
let task_version = task.version.clone();
let use_manifest = task.use_manifest;
let manifest_url = task.manifest_url.clone();
let log_id = format!("install-{}", task_id);
let _ = handle.emit("install-status", InstallProgress {
id: task_id.clone(),
status: "installing".to_string(),
progress: 0.0,
});
let mut args = vec!["install".to_string()];
let display_cmd: String;
let mut temp_manifest_path: Option<PathBuf> = None;
if use_manifest && manifest_url.is_some() {
let url = manifest_url.unwrap();
display_cmd = format!("Winget Install (Manifest): {} from {}", task_id, url);
emit_log(&handle, &log_id, &display_cmd, "Downloading remote manifest...", "info");
let client = reqwest::Client::new();
match client.get(&url).timeout(std::time::Duration::from_secs(15)).send().await {
Ok(resp) if resp.status().is_success() => {
if let Ok(content) = resp.text().await {
let temp_dir = std::env::temp_dir();
let file_name = format!("winget_tmp_{}.yaml", chrono::Local::now().timestamp_millis());
let local_path = temp_dir.join(file_name);
if fs::write(&local_path, content).is_ok() {
args.push("--manifest".to_string());
args.push(local_path.to_string_lossy().to_string());
temp_manifest_path = Some(local_path);
}
}
}
_ => {}
}
if temp_manifest_path.is_none() {
emit_log(&handle, &log_id, "Error", "Failed to download or save manifest.", "error");
let _ = handle.emit("install-status", InstallProgress {
id: task_id.clone(),
status: "error".to_string(),
progress: 0.0,
});
continue;
}
} else {
args.push("--id".to_string());
args.push(task_id.clone());
args.push("-e".to_string());
if let Some(v) = &task_version {
if !v.is_empty() {
args.push("--version".to_string());
args.push(v.clone());
}
}
display_cmd = match &task_version {
Some(v) if !v.is_empty() => format!("Winget Install: {} (v{})", task_id, v),
_ => format!("Winget Install: {}", task_id),
};
}
args.extend([
"--silent".to_string(),
"--accept-package-agreements".to_string(),
"--accept-source-agreements".to_string(),
"--disable-interactivity".to_string(),
]);
let full_command = format!("winget {}", args.join(" "));
emit_log(&handle, &log_id, &display_cmd, &format!("Executing: {}\n---", full_command), "info");
let h = handle.clone();
let current_id = task_id.clone();
let child = Command::new("winget")
.args(&args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.creation_flags(0x08000000)
.spawn();
let status_result = match child {
Ok(mut child_proc) => {
if let Some(stdout) = child_proc.stdout.take() {
let reader = BufReader::new(stdout);
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();
let clean_line = line_str.trim();
if clean_line.is_empty() { continue; }
let mut is_progress = false;
if let Some(caps) = perc_re.captures(clean_line) {
if let Ok(p_val) = caps[1].parse::<f32>() {
let _ = h.emit("install-status", InstallProgress {
id: current_id.clone(),
status: "installing".to_string(),
progress: p_val / 100.0,
});
is_progress = true;
}
} else if let Some(caps) = size_re.captures(clean_line) {
let current = caps[1].parse::<f32>().unwrap_or(0.0);
let total = caps[2].parse::<f32>().unwrap_or(1.0);
if total > 0.0 {
let _ = h.emit("install-status", InstallProgress {
id: current_id.clone(),
status: "installing".to_string(),
progress: (current / total).min(1.0),
});
is_progress = true;
}
}
if !is_progress && clean_line.chars().count() > 1 {
emit_log(&h, &log_id, "", clean_line, "info");
}
}
}
}
let exit_status = child_proc.wait().map(|s| s.success()).unwrap_or(false);
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");
"error"
}
};
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" });
if let Some(path) = temp_manifest_path {
let _ = fs::remove_file(path);
}
}
});
Ok(())
})
.invoke_handler(tauri::generate_handler![
initialize_app,
get_settings,
save_settings,
sync_essentials,
get_essentials,
get_installed_software,
get_updates,
install_software,
get_logs_history
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}