use std::fs; use std::io::{BufRead, BufReader, Read}; use std::os::windows::process::CommandExt; use std::path::PathBuf; use std::process::{Command, Stdio}; use std::thread; use regex::Regex; use tauri::{AppHandle, Emitter, State}; use tokio::sync::mpsc; use winreg::enums::*; use winreg::RegKey; use crate::domain::models::{InstallProgress, InstallTask, TaskEventPayload}; use crate::services::essentials_service; use crate::services::log_service::emit_log; use crate::winget::PostInstallStep; pub struct AppState { pub install_tx: mpsc::Sender, pub app_handle: AppHandle, } pub fn create_install_state(handle: AppHandle) -> AppState { let (tx, mut rx) = mpsc::channel::(100); let runtime_handle = handle.clone(); 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 enable_post_install_flag = task.enable_post_install; let log_id = format!("install-{}", task_id); emit_task_event( &runtime_handle, &log_id, &task_id, "install", "running", "installing", 0.0, task_version.clone(), None, ); let mut args = vec!["install".to_string()]; let display_cmd: String; let mut temp_manifest_path: Option = None; if use_manifest && manifest_url.is_some() { let url = manifest_url.unwrap(); display_cmd = format!("Winget Install (Manifest): {} from {}", task_id, url); emit_task_event( &runtime_handle, &log_id, &task_id, "install", "running", "downloading_manifest", 0.0, task_version.clone(), Some("Downloading remote manifest".to_string()), ); emit_log( &runtime_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( &runtime_handle, &log_id, "Error", "Failed to download or save manifest.", "error", ); emit_task_event( &runtime_handle, &log_id, &task_id, "install", "failed", "manifest_error", 0.0, task_version.clone(), Some("Failed to download or save manifest".to_string()), ); 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( &runtime_handle, &log_id, &display_cmd, &format!("Executing: {}\n---", full_command), "info", ); emit_task_event( &runtime_handle, &log_id, &task_id, "install", "running", "invoking_winget", 0.0, task_version.clone(), None, ); 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) => { let stdout_handle = child_proc.stdout.take().map(|stdout| { spawn_install_stream_reader( stdout, runtime_handle.clone(), log_id.clone(), task_id.clone(), "stdout", perc_re.clone(), size_re.clone(), ) }); let stderr_handle = child_proc.stderr.take().map(|stderr| { spawn_install_stream_reader( stderr, runtime_handle.clone(), log_id.clone(), task_id.clone(), "stderr", perc_re.clone(), size_re.clone(), ) }); let exit_status = child_proc.wait().map(|s| s.success()).unwrap_or(false); if let Some(join_handle) = stdout_handle { let _ = join_handle.join(); } if let Some(join_handle) = stderr_handle { let _ = join_handle.join(); } let status_result = if exit_status { "success" } else { "error" }; if status_result == "success" && enable_post_install_flag { let software_info = essentials_service::get_essentials(&runtime_handle) .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( &runtime_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(text) = resp.text().await { match serde_json::from_str::>(&text) { Ok(steps) => { emit_log( &runtime_handle, &log_id, "Post-Install", &format!( "Successfully fetched remote config with {} steps.", steps.len() ), "info", ); final_steps = Some(steps); } Err(e) => { emit_log( &runtime_handle, &log_id, "Post-Install Error", &format!("JSON Parse Error: {}. Raw Content: {}", e, text), "error", ); } } } } else { emit_log( &runtime_handle, &log_id, "Post-Install Error", &format!("Remote config HTTP Error: {}", resp.status()), "error", ); } } } if let Some(steps) = final_steps { emit_task_event( &runtime_handle, &log_id, &task_id, "install", "running", "configuring", 1.0, task_version.clone(), Some("Starting post-installation configuration".to_string()), ); emit_log( &runtime_handle, &log_id, "Post-Install", "Starting post-installation configuration...", "info", ); if let Err(e) = execute_post_install(&runtime_handle, &log_id, steps).await { emit_log(&runtime_handle, &log_id, "Post-Install Error", &e, "error"); } else { emit_log( &runtime_handle, &log_id, "Post-Install", "Post-installation configuration completed.", "success", ); } } } } status_result } Err(e) => { emit_log(&runtime_handle, &log_id, "Fatal Error", &e.to_string(), "error"); emit_task_event( &runtime_handle, &log_id, &task_id, "install", "failed", "spawn_error", 0.0, task_version.clone(), Some(e.to_string()), ); "error" } }; emit_task_event( &runtime_handle, &log_id, &task_id, "install", if status_result == "success" { "completed" } else { "failed" }, status_result, 1.0, task_version.clone(), Some(format!("Execution finished: {}", status_result)), ); emit_log( &runtime_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); } } }); AppState { install_tx: tx, app_handle: handle } } #[tauri::command] pub async fn install_software( task: InstallTask, state: State<'_, AppState>, ) -> Result<(), String> { let log_id = format!("install-{}", task.id); emit_task_event( &state.app_handle, &log_id, &task.id, "install", "queued", "queued", 0.0, task.version.clone(), None, ); state.install_tx.send(task).await.map_err(|e| e.to_string()) } fn spawn_install_stream_reader( reader: R, handle: AppHandle, log_id: String, task_id: String, stream_name: &'static str, perc_re: Regex, size_re: Regex, ) -> thread::JoinHandle<()> { thread::spawn(move || { let reader = BufReader::new(reader); 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; } if stream_name == "stdout" { let mut is_progress = false; if let Some(caps) = perc_re.captures(clean_line) { if let Ok(p_val) = caps[1].parse::() { let _ = handle.emit( "install-status", InstallProgress { id: task_id.clone(), status: "installing".to_string(), progress: p_val / 100.0, }, ); let _ = handle.emit( "task-event", TaskEventPayload { task_id: log_id.clone(), software_id: task_id.clone(), task_type: "install".to_string(), status: "running".to_string(), stage: "installing".to_string(), progress: p_val / 100.0, target_version: None, message: None, }, ); is_progress = true; } } else if let Some(caps) = size_re.captures(clean_line) { let current = caps[1].parse::().unwrap_or(0.0); let total = caps[2].parse::().unwrap_or(1.0); if total > 0.0 { let _ = handle.emit( "install-status", InstallProgress { id: task_id.clone(), status: "installing".to_string(), progress: (current / total).min(1.0), }, ); let _ = handle.emit( "task-event", TaskEventPayload { task_id: log_id.clone(), software_id: task_id.clone(), task_type: "install".to_string(), status: "running".to_string(), stage: "installing".to_string(), progress: (current / total).min(1.0), target_version: None, message: None, }, ); is_progress = true; } } if !is_progress && clean_line.chars().count() > 1 { emit_log(&handle, &log_id, "", clean_line, "info"); } } else { emit_log(&handle, &log_id, stream_name, clean_line, "error"); } } } }) } fn emit_task_event( handle: &AppHandle, task_id: &str, software_id: &str, task_type: &str, status: &str, stage: &str, progress: f32, target_version: Option, message: Option, ) { let _ = handle.emit( "task-event", TaskEventPayload { task_id: task_id.to_string(), software_id: software_id.to_string(), task_type: task_type.to_string(), status: status.to_string(), stage: stage.to_string(), progress, target_version: target_version.clone(), message, }, ); let legacy_status = match status { "queued" => "pending".to_string(), "completed" => "success".to_string(), "failed" => "error".to_string(), _ => stage.to_string(), }; let _ = handle.emit( "install-status", InstallProgress { id: software_id.to_string(), status: legacy_status, progress, }, ); } fn expand_win_path(path: &str) -> PathBuf { let mut expanded = path.to_string(); let env_vars = [ "AppData", "LocalAppData", "ProgramData", "SystemRoot", "SystemDrive", "TEMP", "USERPROFILE", "HOMEDRIVE", "HOMEPATH", ]; for var in env_vars { let re = Regex::new(&format!(r"(?i)%{}%", var)).unwrap(); if re.is_match(&expanded) { if let Ok(val) = std::env::var(var) { expanded = re.replace_all(&expanded, val.as_str()).to_string(); } } } 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); let delay = match &step { PostInstallStep::RegistryBatch { delay_ms, .. } => *delay_ms, PostInstallStep::FileCopy { delay_ms, .. } => *delay_ms, PostInstallStep::FileDelete { delay_ms, .. } => *delay_ms, PostInstallStep::Command { delay_ms, .. } => *delay_ms, }; 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()) } "Delete" => key.delete_value(&name), _ => Err(std::io::Error::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::FileCopy { src, dest, .. } => { let dest_path = expand_win_path(&dest); let src_is_url = src.starts_with("http://") || src.starts_with("https://"); if src_is_url { emit_log( handle, log_id, "File Download", &format!("{}Downloading {:?} to {:?}...", step_prefix, src, dest_path), "info", ); let client = reqwest::Client::new(); match client .get(&src) .timeout(std::time::Duration::from_secs(60)) .send() .await { Ok(resp) if resp.status().is_success() => { if let Ok(bytes) = resp.bytes().await { if let Some(parent) = dest_path.parent() { let _ = fs::create_dir_all(parent); } if let Err(e) = fs::write(&dest_path, bytes) { emit_log( handle, log_id, "File Error", &format!("Failed to write to {:?}: {}", dest_path, e), "error", ); } else { emit_log( handle, log_id, "Success", "File downloaded and saved 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"), } } else { let src_path = expand_win_path(&src); emit_log( handle, log_id, "File Copy", &format!("{}Copying {:?} to {:?}...", step_prefix, src_path, dest_path), "info", ); if let Some(parent) = dest_path.parent() { let _ = fs::create_dir_all(parent); } if let Err(e) = fs::copy(&src_path, &dest_path) { emit_log( handle, log_id, "File Error", &format!("Failed to copy file: {}", e), "error", ); } else { emit_log(handle, log_id, "Success", "File copied successfully.", "success"); } } } PostInstallStep::FileDelete { path, .. } => { let full_path = expand_win_path(&path); emit_log( handle, log_id, "File Delete", &format!("{}Deleting {:?}...", step_prefix, full_path), "info", ); if full_path.exists() { if let Err(e) = fs::remove_file(&full_path) { emit_log( handle, log_id, "File Error", &format!("Failed to delete file: {}", e), "error", ); } else { emit_log(handle, log_id, "Success", "File deleted successfully.", "success"); } } else { emit_log(handle, log_id, "File Info", "File does not exist, skipping.", "info"); } } PostInstallStep::Command { run, .. } => { emit_log( handle, log_id, "Command Execution", &format!("{}Executing: {}", step_prefix, run), "info", ); let output = Command::new("cmd") .arg("/C") .raw_arg(&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"); } } } } if let Some(ms) = delay { if ms > 0 { emit_log( handle, log_id, "Post-Install", &format!("Waiting for {}ms...", ms), "info", ); tokio::time::sleep(std::time::Duration::from_millis(ms)).await; } } } Ok(()) }