refactor 1

This commit is contained in:
Julian Freeman
2026-04-18 15:55:03 -04:00
parent fe86431899
commit 2aaa330c9a
19 changed files with 1031 additions and 640 deletions

View File

@@ -0,0 +1,641 @@
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};
use crate::services::essentials_service;
use crate::services::log_service::emit_log;
use crate::winget::PostInstallStep;
pub struct AppState {
pub install_tx: mpsc::Sender<InstallTask>,
}
pub fn create_install_state(handle: AppHandle) -> AppState {
let (tx, mut rx) = mpsc::channel::<InstallTask>(100);
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);
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 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,
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,
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(&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(
&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::<Vec<PostInstallStep>>(&text) {
Ok(steps) => {
emit_log(
&handle,
&log_id,
"Post-Install",
&format!(
"Successfully fetched remote config with {} steps.",
steps.len()
),
"info",
);
final_steps = Some(steps);
}
Err(e) => {
emit_log(
&handle,
&log_id,
"Post-Install Error",
&format!("JSON Parse Error: {}. Raw Content: {}", e, text),
"error",
);
}
}
}
} else {
emit_log(
&handle,
&log_id,
"Post-Install Error",
&format!("Remote config HTTP Error: {}", resp.status()),
"error",
);
}
}
}
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(&handle, &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);
}
}
});
AppState { install_tx: tx }
}
#[tauri::command]
pub async fn install_software(
task: InstallTask,
state: State<'_, AppState>,
) -> Result<(), String> {
state.install_tx.send(task).await.map_err(|e| e.to_string())
}
fn spawn_install_stream_reader<R: Read + Send + 'static>(
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::<f32>() {
let _ = handle.emit(
"install-status",
InstallProgress {
id: task_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 _ = handle.emit(
"install-status",
InstallProgress {
id: task_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(&handle, &log_id, "", clean_line, "info");
}
} else {
emit_log(&handle, &log_id, stream_name, clean_line, "error");
}
}
}
})
}
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<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);
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<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::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(())
}

View File

@@ -0,0 +1 @@
pub mod install_queue;