refactor 4
This commit is contained in:
@@ -64,6 +64,18 @@ pub struct ResolvedPostInstall {
|
||||
pub steps: Vec<PostInstallStep>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct TaskEventPayload {
|
||||
pub task_id: String,
|
||||
pub software_id: String,
|
||||
pub task_type: String,
|
||||
pub status: String,
|
||||
pub stage: String,
|
||||
pub progress: f32,
|
||||
pub target_version: Option<String>,
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct EssentialsStatusItem {
|
||||
pub id: String,
|
||||
|
||||
@@ -11,17 +11,19 @@ use tokio::sync::mpsc;
|
||||
use winreg::enums::*;
|
||||
use winreg::RegKey;
|
||||
|
||||
use crate::domain::models::{InstallProgress, InstallTask};
|
||||
use crate::domain::models::{InstallProgress, InstallTask, TaskEventPayload};
|
||||
use crate::services::essentials_service;
|
||||
use crate::services::log_service::emit_log;
|
||||
use crate::winget::PostInstallStep;
|
||||
|
||||
pub struct AppState {
|
||||
pub install_tx: mpsc::Sender<InstallTask>,
|
||||
pub app_handle: AppHandle,
|
||||
}
|
||||
|
||||
pub fn create_install_state(handle: AppHandle) -> AppState {
|
||||
let (tx, mut rx) = mpsc::channel::<InstallTask>(100);
|
||||
let runtime_handle = handle.clone();
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let perc_re = Regex::new(r"(\d+)\s*%").unwrap();
|
||||
@@ -35,14 +37,16 @@ pub fn create_install_state(handle: AppHandle) -> AppState {
|
||||
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,
|
||||
},
|
||||
emit_task_event(
|
||||
&runtime_handle,
|
||||
&log_id,
|
||||
&task_id,
|
||||
"install",
|
||||
"running",
|
||||
"installing",
|
||||
0.0,
|
||||
task_version.clone(),
|
||||
None,
|
||||
);
|
||||
|
||||
let mut args = vec!["install".to_string()];
|
||||
@@ -52,8 +56,19 @@ pub fn create_install_state(handle: AppHandle) -> AppState {
|
||||
if use_manifest && manifest_url.is_some() {
|
||||
let url = manifest_url.unwrap();
|
||||
display_cmd = format!("Winget Install (Manifest): {} from {}", task_id, url);
|
||||
emit_task_event(
|
||||
&runtime_handle,
|
||||
&log_id,
|
||||
&task_id,
|
||||
"install",
|
||||
"running",
|
||||
"downloading_manifest",
|
||||
0.0,
|
||||
task_version.clone(),
|
||||
Some("Downloading remote manifest".to_string()),
|
||||
);
|
||||
emit_log(
|
||||
&handle,
|
||||
&runtime_handle,
|
||||
&log_id,
|
||||
&display_cmd,
|
||||
"Downloading remote manifest...",
|
||||
@@ -85,19 +100,22 @@ pub fn create_install_state(handle: AppHandle) -> AppState {
|
||||
|
||||
if temp_manifest_path.is_none() {
|
||||
emit_log(
|
||||
&handle,
|
||||
&runtime_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,
|
||||
},
|
||||
emit_task_event(
|
||||
&runtime_handle,
|
||||
&log_id,
|
||||
&task_id,
|
||||
"install",
|
||||
"failed",
|
||||
"manifest_error",
|
||||
0.0,
|
||||
task_version.clone(),
|
||||
Some("Failed to download or save manifest".to_string()),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
@@ -128,12 +146,23 @@ pub fn create_install_state(handle: AppHandle) -> AppState {
|
||||
|
||||
let full_command = format!("winget {}", args.join(" "));
|
||||
emit_log(
|
||||
&handle,
|
||||
&runtime_handle,
|
||||
&log_id,
|
||||
&display_cmd,
|
||||
&format!("Executing: {}\n---", full_command),
|
||||
"info",
|
||||
);
|
||||
emit_task_event(
|
||||
&runtime_handle,
|
||||
&log_id,
|
||||
&task_id,
|
||||
"install",
|
||||
"running",
|
||||
"invoking_winget",
|
||||
0.0,
|
||||
task_version.clone(),
|
||||
None,
|
||||
);
|
||||
|
||||
let child = Command::new("winget")
|
||||
.args(&args)
|
||||
@@ -147,7 +176,7 @@ pub fn create_install_state(handle: AppHandle) -> AppState {
|
||||
let stdout_handle = child_proc.stdout.take().map(|stdout| {
|
||||
spawn_install_stream_reader(
|
||||
stdout,
|
||||
handle.clone(),
|
||||
runtime_handle.clone(),
|
||||
log_id.clone(),
|
||||
task_id.clone(),
|
||||
"stdout",
|
||||
@@ -158,7 +187,7 @@ pub fn create_install_state(handle: AppHandle) -> AppState {
|
||||
let stderr_handle = child_proc.stderr.take().map(|stderr| {
|
||||
spawn_install_stream_reader(
|
||||
stderr,
|
||||
handle.clone(),
|
||||
runtime_handle.clone(),
|
||||
log_id.clone(),
|
||||
task_id.clone(),
|
||||
"stderr",
|
||||
@@ -177,7 +206,7 @@ pub fn create_install_state(handle: AppHandle) -> AppState {
|
||||
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)
|
||||
let software_info = essentials_service::get_essentials(&runtime_handle)
|
||||
.and_then(|repo| repo.essentials.into_iter().find(|s| s.id == task_id));
|
||||
|
||||
if let Some(sw) = software_info {
|
||||
@@ -188,7 +217,7 @@ pub fn create_install_state(handle: AppHandle) -> AppState {
|
||||
}
|
||||
} else if let Some(url) = sw.post_install_url {
|
||||
emit_log(
|
||||
&handle,
|
||||
&runtime_handle,
|
||||
&log_id,
|
||||
"Post-Install",
|
||||
"Local config not found, fetching remote config...",
|
||||
@@ -206,7 +235,7 @@ pub fn create_install_state(handle: AppHandle) -> AppState {
|
||||
match serde_json::from_str::<Vec<PostInstallStep>>(&text) {
|
||||
Ok(steps) => {
|
||||
emit_log(
|
||||
&handle,
|
||||
&runtime_handle,
|
||||
&log_id,
|
||||
"Post-Install",
|
||||
&format!(
|
||||
@@ -219,7 +248,7 @@ pub fn create_install_state(handle: AppHandle) -> AppState {
|
||||
}
|
||||
Err(e) => {
|
||||
emit_log(
|
||||
&handle,
|
||||
&runtime_handle,
|
||||
&log_id,
|
||||
"Post-Install Error",
|
||||
&format!("JSON Parse Error: {}. Raw Content: {}", e, text),
|
||||
@@ -230,7 +259,7 @@ pub fn create_install_state(handle: AppHandle) -> AppState {
|
||||
}
|
||||
} else {
|
||||
emit_log(
|
||||
&handle,
|
||||
&runtime_handle,
|
||||
&log_id,
|
||||
"Post-Install Error",
|
||||
&format!("Remote config HTTP Error: {}", resp.status()),
|
||||
@@ -241,26 +270,29 @@ pub fn create_install_state(handle: AppHandle) -> AppState {
|
||||
}
|
||||
|
||||
if let Some(steps) = final_steps {
|
||||
let _ = handle.emit(
|
||||
"install-status",
|
||||
InstallProgress {
|
||||
id: task_id.clone(),
|
||||
status: "configuring".to_string(),
|
||||
progress: 1.0,
|
||||
},
|
||||
emit_task_event(
|
||||
&runtime_handle,
|
||||
&log_id,
|
||||
&task_id,
|
||||
"install",
|
||||
"running",
|
||||
"configuring",
|
||||
1.0,
|
||||
task_version.clone(),
|
||||
Some("Starting post-installation configuration".to_string()),
|
||||
);
|
||||
emit_log(
|
||||
&handle,
|
||||
&runtime_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");
|
||||
if let Err(e) = execute_post_install(&runtime_handle, &log_id, steps).await {
|
||||
emit_log(&runtime_handle, &log_id, "Post-Install Error", &e, "error");
|
||||
} else {
|
||||
emit_log(
|
||||
&handle,
|
||||
&runtime_handle,
|
||||
&log_id,
|
||||
"Post-Install",
|
||||
"Post-installation configuration completed.",
|
||||
@@ -273,21 +305,35 @@ pub fn create_install_state(handle: AppHandle) -> AppState {
|
||||
status_result
|
||||
}
|
||||
Err(e) => {
|
||||
emit_log(&handle, &log_id, "Fatal Error", &e.to_string(), "error");
|
||||
emit_log(&runtime_handle, &log_id, "Fatal Error", &e.to_string(), "error");
|
||||
emit_task_event(
|
||||
&runtime_handle,
|
||||
&log_id,
|
||||
&task_id,
|
||||
"install",
|
||||
"failed",
|
||||
"spawn_error",
|
||||
0.0,
|
||||
task_version.clone(),
|
||||
Some(e.to_string()),
|
||||
);
|
||||
"error"
|
||||
}
|
||||
};
|
||||
|
||||
let _ = handle.emit(
|
||||
"install-status",
|
||||
InstallProgress {
|
||||
id: task_id.clone(),
|
||||
status: status_result.to_string(),
|
||||
progress: 1.0,
|
||||
},
|
||||
emit_task_event(
|
||||
&runtime_handle,
|
||||
&log_id,
|
||||
&task_id,
|
||||
"install",
|
||||
if status_result == "success" { "completed" } else { "failed" },
|
||||
status_result,
|
||||
1.0,
|
||||
task_version.clone(),
|
||||
Some(format!("Execution finished: {}", status_result)),
|
||||
);
|
||||
emit_log(
|
||||
&handle,
|
||||
&runtime_handle,
|
||||
&log_id,
|
||||
"Result",
|
||||
&format!("Execution finished: {}", status_result),
|
||||
@@ -304,7 +350,7 @@ pub fn create_install_state(handle: AppHandle) -> AppState {
|
||||
}
|
||||
});
|
||||
|
||||
AppState { install_tx: tx }
|
||||
AppState { install_tx: tx, app_handle: handle }
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -312,6 +358,18 @@ pub async fn install_software(
|
||||
task: InstallTask,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let log_id = format!("install-{}", task.id);
|
||||
emit_task_event(
|
||||
&state.app_handle,
|
||||
&log_id,
|
||||
&task.id,
|
||||
"install",
|
||||
"queued",
|
||||
"queued",
|
||||
0.0,
|
||||
task.version.clone(),
|
||||
None,
|
||||
);
|
||||
state.install_tx.send(task).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
@@ -346,6 +404,19 @@ fn spawn_install_stream_reader<R: Read + Send + 'static>(
|
||||
progress: p_val / 100.0,
|
||||
},
|
||||
);
|
||||
let _ = handle.emit(
|
||||
"task-event",
|
||||
TaskEventPayload {
|
||||
task_id: log_id.clone(),
|
||||
software_id: task_id.clone(),
|
||||
task_type: "install".to_string(),
|
||||
status: "running".to_string(),
|
||||
stage: "installing".to_string(),
|
||||
progress: p_val / 100.0,
|
||||
target_version: None,
|
||||
message: None,
|
||||
},
|
||||
);
|
||||
is_progress = true;
|
||||
}
|
||||
} else if let Some(caps) = size_re.captures(clean_line) {
|
||||
@@ -360,6 +431,19 @@ fn spawn_install_stream_reader<R: Read + Send + 'static>(
|
||||
progress: (current / total).min(1.0),
|
||||
},
|
||||
);
|
||||
let _ = handle.emit(
|
||||
"task-event",
|
||||
TaskEventPayload {
|
||||
task_id: log_id.clone(),
|
||||
software_id: task_id.clone(),
|
||||
task_type: "install".to_string(),
|
||||
status: "running".to_string(),
|
||||
stage: "installing".to_string(),
|
||||
progress: (current / total).min(1.0),
|
||||
target_version: None,
|
||||
message: None,
|
||||
},
|
||||
);
|
||||
is_progress = true;
|
||||
}
|
||||
}
|
||||
@@ -375,6 +459,48 @@ fn spawn_install_stream_reader<R: Read + Send + 'static>(
|
||||
})
|
||||
}
|
||||
|
||||
fn emit_task_event(
|
||||
handle: &AppHandle,
|
||||
task_id: &str,
|
||||
software_id: &str,
|
||||
task_type: &str,
|
||||
status: &str,
|
||||
stage: &str,
|
||||
progress: f32,
|
||||
target_version: Option<String>,
|
||||
message: Option<String>,
|
||||
) {
|
||||
let _ = handle.emit(
|
||||
"task-event",
|
||||
TaskEventPayload {
|
||||
task_id: task_id.to_string(),
|
||||
software_id: software_id.to_string(),
|
||||
task_type: task_type.to_string(),
|
||||
status: status.to_string(),
|
||||
stage: stage.to_string(),
|
||||
progress,
|
||||
target_version: target_version.clone(),
|
||||
message,
|
||||
},
|
||||
);
|
||||
|
||||
let legacy_status = match status {
|
||||
"queued" => "pending".to_string(),
|
||||
"completed" => "success".to_string(),
|
||||
"failed" => "error".to_string(),
|
||||
_ => stage.to_string(),
|
||||
};
|
||||
|
||||
let _ = handle.emit(
|
||||
"install-status",
|
||||
InstallProgress {
|
||||
id: software_id.to_string(),
|
||||
status: legacy_status,
|
||||
progress,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn expand_win_path(path: &str) -> PathBuf {
|
||||
let mut expanded = path.to_string();
|
||||
let env_vars = [
|
||||
|
||||
@@ -3,11 +3,11 @@ import { invoke } from '@tauri-apps/api/core'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
|
||||
import { useCatalogStore } from './catalog'
|
||||
import type { ActiveTaskState, LogEntry } from './types'
|
||||
import type { ActiveTaskState, LogEntry, TaskEventPayload, TaskRecord } from './types'
|
||||
|
||||
export const useTaskRuntimeStore = defineStore('task-runtime', {
|
||||
state: () => ({
|
||||
activeTasks: {} as Record<string, ActiveTaskState>,
|
||||
taskRecords: {} as Record<string, TaskRecord>,
|
||||
selectedEssentialIds: [] as string[],
|
||||
selectedUpdateIds: [] as string[],
|
||||
logs: [] as LogEntry[],
|
||||
@@ -16,9 +16,21 @@ export const useTaskRuntimeStore = defineStore('task-runtime', {
|
||||
postInstallPrefs: {} as Record<string, boolean>
|
||||
}),
|
||||
getters: {
|
||||
isTaskBusy: (state) => Object.values(state.activeTasks).some(task =>
|
||||
task.status === 'pending' || task.status === 'installing'
|
||||
activeTasks: (state): Record<string, ActiveTaskState> => {
|
||||
return Object.values(state.taskRecords).reduce<Record<string, ActiveTaskState>>((acc, task) => {
|
||||
acc[task.softwareId] = {
|
||||
status: mapTaskToLegacyStatus(task),
|
||||
progress: task.progress,
|
||||
targetVersion: task.targetVersion
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
},
|
||||
isTaskBusy(): boolean {
|
||||
return Object.values(this.taskRecords).some(task =>
|
||||
task.status === 'queued' || task.status === 'running'
|
||||
)
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
toggleSelection(id: string, type: 'essential' | 'update') {
|
||||
@@ -48,7 +60,6 @@ export const useTaskRuntimeStore = defineStore('task-runtime', {
|
||||
if (!software) return
|
||||
|
||||
const enablePostInstall = this.postInstallPrefs[id] !== false
|
||||
this.activeTasks[id] = { status: 'pending', progress: 0, targetVersion }
|
||||
try {
|
||||
await invoke('install_software', {
|
||||
task: {
|
||||
@@ -61,7 +72,16 @@ export const useTaskRuntimeStore = defineStore('task-runtime', {
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Invoke install failed:', err)
|
||||
this.activeTasks[id] = { status: 'error', progress: 0 }
|
||||
this.taskRecords[`install-${id}`] = {
|
||||
taskId: `install-${id}`,
|
||||
softwareId: id,
|
||||
taskType: 'install',
|
||||
status: 'failed',
|
||||
stage: 'invoke_error',
|
||||
progress: 0,
|
||||
targetVersion,
|
||||
message: String(err)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -71,10 +91,10 @@ export const useTaskRuntimeStore = defineStore('task-runtime', {
|
||||
|
||||
this.refreshTimer = setTimeout(async () => {
|
||||
await catalog.fetchAllData()
|
||||
Object.keys(this.activeTasks).forEach(id => {
|
||||
const status = this.activeTasks[id].status
|
||||
if (status === 'success' || status === 'error') {
|
||||
delete this.activeTasks[id]
|
||||
Object.keys(this.taskRecords).forEach(taskId => {
|
||||
const status = this.taskRecords[taskId].status
|
||||
if (status === 'completed' || status === 'failed') {
|
||||
delete this.taskRecords[taskId]
|
||||
}
|
||||
})
|
||||
this.refreshTimer = null
|
||||
@@ -85,19 +105,28 @@ export const useTaskRuntimeStore = defineStore('task-runtime', {
|
||||
if ((window as Window & { __tauri_listener_init?: boolean }).__tauri_listener_init) return
|
||||
;(window as Window & { __tauri_listener_init?: boolean }).__tauri_listener_init = true
|
||||
|
||||
listen('install-status', async (event: { payload: { id: string, status: string, progress: number } }) => {
|
||||
listen('task-event', async (event: { payload: TaskEventPayload }) => {
|
||||
const catalog = useCatalogStore()
|
||||
const { id, status, progress } = event.payload
|
||||
const task = this.activeTasks[id]
|
||||
const payload = event.payload
|
||||
const taskRecord: TaskRecord = {
|
||||
taskId: payload.task_id,
|
||||
softwareId: payload.software_id,
|
||||
taskType: payload.task_type,
|
||||
status: payload.status,
|
||||
stage: payload.stage,
|
||||
progress: payload.progress,
|
||||
targetVersion: payload.target_version ?? undefined,
|
||||
message: payload.message ?? undefined
|
||||
}
|
||||
|
||||
this.activeTasks[id] = { status, progress, targetVersion: task?.targetVersion }
|
||||
this.taskRecords[payload.task_id] = taskRecord
|
||||
|
||||
if (status === 'success' || status === 'error') {
|
||||
if (status === 'success') {
|
||||
if (payload.status === 'completed' || payload.status === 'failed') {
|
||||
if (payload.status === 'completed') {
|
||||
try {
|
||||
const latestInfo = await invoke('get_software_info', { id }) as Record<string, unknown> | null
|
||||
const latestInfo = await invoke('get_software_info', { id: payload.software_id }) as Record<string, unknown> | null
|
||||
if (latestInfo) {
|
||||
const index = catalog.allSoftware.findIndex(s => s.id.toLowerCase() === id.toLowerCase())
|
||||
const index = catalog.allSoftware.findIndex(s => s.id.toLowerCase() === payload.software_id.toLowerCase())
|
||||
if (index !== -1) {
|
||||
catalog.allSoftware[index] = { ...catalog.allSoftware[index], ...latestInfo }
|
||||
} else {
|
||||
@@ -108,17 +137,17 @@ export const useTaskRuntimeStore = defineStore('task-runtime', {
|
||||
console.error('Partial refresh failed:', err)
|
||||
}
|
||||
|
||||
this.selectedEssentialIds = this.selectedEssentialIds.filter(item => item !== id)
|
||||
this.selectedUpdateIds = this.selectedUpdateIds.filter(item => item !== id)
|
||||
this.selectedEssentialIds = this.selectedEssentialIds.filter(item => item !== payload.software_id)
|
||||
this.selectedUpdateIds = this.selectedUpdateIds.filter(item => item !== payload.software_id)
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.activeTasks[id]?.status === 'success') {
|
||||
delete this.activeTasks[id]
|
||||
if (this.taskRecords[payload.task_id]?.status === 'completed') {
|
||||
delete this.taskRecords[payload.task_id]
|
||||
}
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
const index = this.batchQueue.indexOf(id)
|
||||
const index = this.batchQueue.indexOf(payload.software_id)
|
||||
if (index !== -1) {
|
||||
this.batchQueue.splice(index, 1)
|
||||
if (this.batchQueue.length === 0) {
|
||||
@@ -128,6 +157,11 @@ export const useTaskRuntimeStore = defineStore('task-runtime', {
|
||||
}
|
||||
})
|
||||
|
||||
listen('install-status', () => {
|
||||
// Compatibility event is still emitted by the backend, but task-runtime
|
||||
// now derives runtime state from the richer task-event stream.
|
||||
})
|
||||
|
||||
listen('log-event', (event: { payload: LogEntry }) => {
|
||||
const payload = event.payload
|
||||
const existingLog = this.logs.find(item => item.id === payload.id)
|
||||
@@ -142,3 +176,11 @@ export const useTaskRuntimeStore = defineStore('task-runtime', {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function mapTaskToLegacyStatus(task: TaskRecord): string {
|
||||
if (task.status === 'queued') return 'pending'
|
||||
if (task.status === 'completed') return 'success'
|
||||
if (task.status === 'failed') return 'error'
|
||||
if (task.stage === 'configuring') return 'configuring'
|
||||
return 'installing'
|
||||
}
|
||||
|
||||
@@ -47,6 +47,28 @@ export interface ActiveTaskState {
|
||||
targetVersion?: string
|
||||
}
|
||||
|
||||
export interface TaskRecord {
|
||||
taskId: string
|
||||
softwareId: string
|
||||
taskType: string
|
||||
status: string
|
||||
stage: string
|
||||
progress: number
|
||||
targetVersion?: string
|
||||
message?: string
|
||||
}
|
||||
|
||||
export interface TaskEventPayload {
|
||||
task_id: string
|
||||
software_id: string
|
||||
task_type: string
|
||||
status: string
|
||||
stage: string
|
||||
progress: number
|
||||
target_version?: string | null
|
||||
message?: string | null
|
||||
}
|
||||
|
||||
export interface AppSettings {
|
||||
repo_url: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user