refactor 1
This commit is contained in:
55
src-tauri/src/commands/app_commands.rs
Normal file
55
src-tauri/src/commands/app_commands.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use tauri::AppHandle;
|
||||
|
||||
use crate::domain::models::{AppSettings, EssentialsRepo, LogPayload, SyncEssentialsResult};
|
||||
use crate::services::{essentials_service, settings_service, software_state_service};
|
||||
use crate::winget::Software;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_settings(app: AppHandle) -> AppSettings {
|
||||
settings_service::get_settings(&app)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn save_settings(app: AppHandle, settings: AppSettings) -> Result<(), String> {
|
||||
settings_service::save_settings(&app, &settings)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn sync_essentials(app: AppHandle) -> Result<SyncEssentialsResult, String> {
|
||||
essentials_service::sync_essentials(&app).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_essentials(app: AppHandle) -> Option<EssentialsRepo> {
|
||||
essentials_service::get_essentials(&app)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn initialize_app(app: AppHandle) -> Result<bool, String> {
|
||||
software_state_service::initialize_app(app).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_installed_software(app: AppHandle) -> Vec<Software> {
|
||||
software_state_service::get_installed_software(app).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_updates(app: AppHandle) -> Vec<Software> {
|
||||
software_state_service::get_updates(app).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_software_info(app: AppHandle, id: String) -> Option<Software> {
|
||||
software_state_service::get_software_info(app, id).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_software_icon(app: AppHandle, id: String, name: String) -> Option<String> {
|
||||
software_state_service::get_software_icon(app, id, name).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_logs_history() -> Vec<LogPayload> {
|
||||
vec![]
|
||||
}
|
||||
1
src-tauri/src/commands/mod.rs
Normal file
1
src-tauri/src/commands/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod app_commands;
|
||||
1
src-tauri/src/domain/mod.rs
Normal file
1
src-tauri/src/domain/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod models;
|
||||
65
src-tauri/src/domain/models.rs
Normal file
65
src-tauri/src/domain/models.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::winget::{PostInstallStep, Software};
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct AppSettings {
|
||||
pub repo_url: String,
|
||||
}
|
||||
|
||||
impl Default for AppSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
repo_url: "https://karlblue.github.io/winget-repo".to_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>,
|
||||
#[serde(default = "default_true")]
|
||||
pub enable_post_install: bool,
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
pub struct LogPayload {
|
||||
pub id: String,
|
||||
pub timestamp: String,
|
||||
pub command: String,
|
||||
pub output: String,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct SyncEssentialsResult {
|
||||
pub status: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
pub struct InstallProgress {
|
||||
pub id: String,
|
||||
pub status: String,
|
||||
pub progress: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ResolvedPostInstall {
|
||||
pub software: Software,
|
||||
pub steps: Vec<PostInstallStep>,
|
||||
}
|
||||
@@ -1,651 +1,33 @@
|
||||
use tauri::Manager;
|
||||
|
||||
pub mod commands;
|
||||
pub mod domain;
|
||||
pub mod providers;
|
||||
pub mod services;
|
||||
pub mod storage;
|
||||
pub mod tasks;
|
||||
pub mod winget;
|
||||
|
||||
use std::fs;
|
||||
use std::io::Read;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::os::windows::process::CommandExt;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::path::PathBuf;
|
||||
use std::thread;
|
||||
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, get_cached_or_extract_icon};
|
||||
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>,
|
||||
#[serde(default = "default_true")]
|
||||
pub enable_post_install: bool,
|
||||
}
|
||||
|
||||
fn default_true() -> bool { true }
|
||||
|
||||
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, Deserialize)]
|
||||
pub struct SyncEssentialsResult {
|
||||
pub status: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[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<SyncEssentialsResult, String> {
|
||||
let settings = get_settings(app.clone());
|
||||
let url = format!("{}/setup-essentials.json", settings.repo_url.trim_end_matches('/'));
|
||||
let cache_path = get_essentials_path(&app);
|
||||
|
||||
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())?;
|
||||
let validation: Result<EssentialsRepo, _> = serde_json::from_str(&content);
|
||||
if validation.is_ok() {
|
||||
fs::write(cache_path, content).map_err(|e| e.to_string())?;
|
||||
emit_log(&app, "sync-essentials", "Result", "Essentials list updated successfully.", "success");
|
||||
Ok(SyncEssentialsResult {
|
||||
status: "updated".to_string(),
|
||||
message: "清单同步成功".to_string(),
|
||||
})
|
||||
} 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) => {
|
||||
if cache_path.exists() {
|
||||
emit_log(&app, "sync-essentials", "Skipped", &format!("Network issue: {}. Using local cache.", e), "info");
|
||||
Ok(SyncEssentialsResult {
|
||||
status: "cache_used".to_string(),
|
||||
message: "网络不可用,已继续使用本地缓存".to_string(),
|
||||
})
|
||||
} else {
|
||||
let err_msg = format!("Network issue: {}", e);
|
||||
emit_log(&app, "sync-essentials", "Error", &err_msg, "error");
|
||||
Err(err_msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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 get_software_icon(app: AppHandle, id: String, name: String) -> Option<String> {
|
||||
tokio::task::spawn_blocking(move || get_cached_or_extract_icon(&app, &id, &name)).await.unwrap_or(None)
|
||||
}
|
||||
|
||||
#[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]
|
||||
async fn get_software_info(app: AppHandle, id: String) -> Option<Software> {
|
||||
tokio::task::spawn_blocking(move || winget::get_software_info(&app, &id)).await.unwrap_or(None)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_logs_history() -> Vec<LogPayload> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
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::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::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(())
|
||||
}
|
||||
|
||||
#[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 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 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(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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let install_state = tasks::install_queue::create_install_state(app.handle().clone());
|
||||
app.manage(install_state);
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
initialize_app,
|
||||
get_settings,
|
||||
save_settings,
|
||||
sync_essentials,
|
||||
get_essentials,
|
||||
get_installed_software,
|
||||
get_updates,
|
||||
get_software_icon,
|
||||
get_software_info,
|
||||
install_software,
|
||||
get_logs_history
|
||||
commands::app_commands::initialize_app,
|
||||
commands::app_commands::get_settings,
|
||||
commands::app_commands::save_settings,
|
||||
commands::app_commands::sync_essentials,
|
||||
commands::app_commands::get_essentials,
|
||||
commands::app_commands::get_installed_software,
|
||||
commands::app_commands::get_updates,
|
||||
commands::app_commands::get_software_icon,
|
||||
commands::app_commands::get_software_info,
|
||||
tasks::install_queue::install_software,
|
||||
commands::app_commands::get_logs_history
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
1
src-tauri/src/providers/mod.rs
Normal file
1
src-tauri/src/providers/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod winget_client;
|
||||
23
src-tauri/src/providers/winget_client.rs
Normal file
23
src-tauri/src/providers/winget_client.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use tauri::AppHandle;
|
||||
|
||||
use crate::winget::{self, Software};
|
||||
|
||||
pub fn ensure_environment_ready(handle: &AppHandle) -> Result<(), String> {
|
||||
winget::ensure_winget_dependencies(handle)
|
||||
}
|
||||
|
||||
pub fn list_installed_packages(handle: &AppHandle) -> Vec<Software> {
|
||||
winget::list_installed_software(handle)
|
||||
}
|
||||
|
||||
pub fn list_upgrade_candidates(handle: &AppHandle) -> Vec<Software> {
|
||||
winget::list_updates(handle)
|
||||
}
|
||||
|
||||
pub fn get_package_by_id(handle: &AppHandle, id: &str) -> Option<Software> {
|
||||
winget::get_software_info(handle, id)
|
||||
}
|
||||
|
||||
pub fn resolve_icon(handle: &AppHandle, id: &str, name: &str) -> Option<String> {
|
||||
winget::get_cached_or_extract_icon(handle, id, name)
|
||||
}
|
||||
85
src-tauri/src/services/essentials_service.rs
Normal file
85
src-tauri/src/services/essentials_service.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use reqwest::Client;
|
||||
use tauri::AppHandle;
|
||||
|
||||
use crate::domain::models::{EssentialsRepo, SyncEssentialsResult};
|
||||
use crate::services::log_service::emit_log;
|
||||
use crate::services::settings_service;
|
||||
use crate::storage::{essentials_store, paths};
|
||||
|
||||
pub async fn sync_essentials(app: &AppHandle) -> Result<SyncEssentialsResult, String> {
|
||||
let settings = settings_service::get_settings(app);
|
||||
let url = format!("{}/setup-essentials.json", settings.repo_url.trim_end_matches('/'));
|
||||
let cache_path = paths::get_essentials_path(app);
|
||||
|
||||
emit_log(
|
||||
app,
|
||||
"sync-essentials",
|
||||
"Syncing Essentials",
|
||||
&format!("Downloading from {}...", url),
|
||||
"info",
|
||||
);
|
||||
|
||||
let client = 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())?;
|
||||
let validation: Result<EssentialsRepo, _> = serde_json::from_str(&content);
|
||||
if validation.is_ok() {
|
||||
essentials_store::save_essentials(app, &content)?;
|
||||
emit_log(
|
||||
app,
|
||||
"sync-essentials",
|
||||
"Result",
|
||||
"Essentials list updated successfully.",
|
||||
"success",
|
||||
);
|
||||
Ok(SyncEssentialsResult {
|
||||
status: "updated".to_string(),
|
||||
message: "清单同步成功".to_string(),
|
||||
})
|
||||
} 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) => {
|
||||
if cache_path.exists() {
|
||||
emit_log(
|
||||
app,
|
||||
"sync-essentials",
|
||||
"Skipped",
|
||||
&format!("Network issue: {}. Using local cache.", e),
|
||||
"info",
|
||||
);
|
||||
Ok(SyncEssentialsResult {
|
||||
status: "cache_used".to_string(),
|
||||
message: "网络不可用,已继续使用本地缓存".to_string(),
|
||||
})
|
||||
} else {
|
||||
let err_msg = format!("Network issue: {}", e);
|
||||
emit_log(app, "sync-essentials", "Error", &err_msg, "error");
|
||||
Err(err_msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_essentials(app: &AppHandle) -> Option<EssentialsRepo> {
|
||||
essentials_store::load_essentials(app)
|
||||
}
|
||||
17
src-tauri/src/services/log_service.rs
Normal file
17
src-tauri/src/services/log_service.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use tauri::{AppHandle, Emitter};
|
||||
|
||||
use crate::domain::models::LogPayload;
|
||||
|
||||
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(),
|
||||
},
|
||||
);
|
||||
}
|
||||
4
src-tauri/src/services/mod.rs
Normal file
4
src-tauri/src/services/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod essentials_service;
|
||||
pub mod log_service;
|
||||
pub mod settings_service;
|
||||
pub mod software_state_service;
|
||||
12
src-tauri/src/services/settings_service.rs
Normal file
12
src-tauri/src/services/settings_service.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use tauri::AppHandle;
|
||||
|
||||
use crate::domain::models::AppSettings;
|
||||
use crate::storage::settings_store;
|
||||
|
||||
pub fn get_settings(app: &AppHandle) -> AppSettings {
|
||||
settings_store::get_settings(app)
|
||||
}
|
||||
|
||||
pub fn save_settings(app: &AppHandle, settings: &AppSettings) -> Result<(), String> {
|
||||
settings_store::save_settings(app, settings)
|
||||
}
|
||||
35
src-tauri/src/services/software_state_service.rs
Normal file
35
src-tauri/src/services/software_state_service.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use tauri::AppHandle;
|
||||
|
||||
use crate::providers::winget_client;
|
||||
use crate::winget::Software;
|
||||
|
||||
pub async fn initialize_app(app: AppHandle) -> Result<bool, String> {
|
||||
let app_clone = app.clone();
|
||||
tokio::task::spawn_blocking(move || winget_client::ensure_environment_ready(&app_clone).map(|_| true))
|
||||
.await
|
||||
.unwrap_or(Err("Initialization Task Panicked".to_string()))
|
||||
}
|
||||
|
||||
pub async fn get_installed_software(app: AppHandle) -> Vec<Software> {
|
||||
tokio::task::spawn_blocking(move || winget_client::list_installed_packages(&app))
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub async fn get_updates(app: AppHandle) -> Vec<Software> {
|
||||
tokio::task::spawn_blocking(move || winget_client::list_upgrade_candidates(&app))
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub async fn get_software_info(app: AppHandle, id: String) -> Option<Software> {
|
||||
tokio::task::spawn_blocking(move || winget_client::get_package_by_id(&app, &id))
|
||||
.await
|
||||
.unwrap_or(None)
|
||||
}
|
||||
|
||||
pub async fn get_software_icon(app: AppHandle, id: String, name: String) -> Option<String> {
|
||||
tokio::task::spawn_blocking(move || winget_client::resolve_icon(&app, &id, &name))
|
||||
.await
|
||||
.unwrap_or(None)
|
||||
}
|
||||
21
src-tauri/src/storage/essentials_store.rs
Normal file
21
src-tauri/src/storage/essentials_store.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use std::fs;
|
||||
|
||||
use tauri::AppHandle;
|
||||
|
||||
use crate::domain::models::EssentialsRepo;
|
||||
use crate::storage::paths::get_essentials_path;
|
||||
|
||||
pub fn load_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()
|
||||
}
|
||||
|
||||
pub fn save_essentials(app: &AppHandle, content: &str) -> Result<(), String> {
|
||||
let path = get_essentials_path(app);
|
||||
fs::write(path, content).map_err(|e| e.to_string())
|
||||
}
|
||||
3
src-tauri/src/storage/mod.rs
Normal file
3
src-tauri/src/storage/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod essentials_store;
|
||||
pub mod paths;
|
||||
pub mod settings_store;
|
||||
20
src-tauri/src/storage/paths.rs
Normal file
20
src-tauri/src/storage/paths.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
pub fn get_app_data_dir(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
|
||||
}
|
||||
|
||||
pub fn get_settings_path(app: &AppHandle) -> PathBuf {
|
||||
get_app_data_dir(app).join("settings.json")
|
||||
}
|
||||
|
||||
pub fn get_essentials_path(app: &AppHandle) -> PathBuf {
|
||||
get_app_data_dir(app).join("setup-essentials.json")
|
||||
}
|
||||
24
src-tauri/src/storage/settings_store.rs
Normal file
24
src-tauri/src/storage/settings_store.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use std::fs;
|
||||
|
||||
use tauri::AppHandle;
|
||||
|
||||
use crate::domain::models::AppSettings;
|
||||
use crate::storage::paths::get_settings_path;
|
||||
|
||||
pub 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()
|
||||
}
|
||||
|
||||
pub 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())
|
||||
}
|
||||
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(())
|
||||
}
|
||||
1
src-tauri/src/tasks/mod.rs
Normal file
1
src-tauri/src/tasks/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod install_queue;
|
||||
@@ -6,7 +6,7 @@ use std::os::windows::process::CommandExt;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use tauri::{AppHandle, Manager};
|
||||
use crate::emit_log;
|
||||
use crate::services::log_service::emit_log;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct RegistryValue {
|
||||
|
||||
Reference in New Issue
Block a user