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;
|
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() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.setup(move |app| {
|
.setup(move |app| {
|
||||||
let handle = app.handle().clone();
|
let install_state = tasks::install_queue::create_install_state(app.handle().clone());
|
||||||
let (tx, mut rx) = mpsc::channel::<InstallTask>(100);
|
app.manage(install_state);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
initialize_app,
|
commands::app_commands::initialize_app,
|
||||||
get_settings,
|
commands::app_commands::get_settings,
|
||||||
save_settings,
|
commands::app_commands::save_settings,
|
||||||
sync_essentials,
|
commands::app_commands::sync_essentials,
|
||||||
get_essentials,
|
commands::app_commands::get_essentials,
|
||||||
get_installed_software,
|
commands::app_commands::get_installed_software,
|
||||||
get_updates,
|
commands::app_commands::get_updates,
|
||||||
get_software_icon,
|
commands::app_commands::get_software_icon,
|
||||||
get_software_info,
|
commands::app_commands::get_software_info,
|
||||||
install_software,
|
tasks::install_queue::install_software,
|
||||||
get_logs_history
|
commands::app_commands::get_logs_history
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.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::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tauri::{AppHandle, Manager};
|
use tauri::{AppHandle, Manager};
|
||||||
use crate::emit_log;
|
use crate::services::log_service::emit_log;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct RegistryValue {
|
pub struct RegistryValue {
|
||||||
|
|||||||
Reference in New Issue
Block a user