refactor 1
This commit is contained in:
641
src-tauri/src/tasks/install_queue.rs
Normal file
641
src-tauri/src/tasks/install_queue.rs
Normal 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(())
|
||||
}
|
||||
Reference in New Issue
Block a user