refactor 4

This commit is contained in:
Julian Freeman
2026-04-18 16:05:00 -04:00
parent 0fc523e234
commit 2625c8b52f
4 changed files with 273 additions and 71 deletions

View File

@@ -64,6 +64,18 @@ pub struct ResolvedPostInstall {
pub steps: Vec<PostInstallStep>, 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)] #[derive(Clone, Serialize, Deserialize)]
pub struct EssentialsStatusItem { pub struct EssentialsStatusItem {
pub id: String, pub id: String,

View File

@@ -11,17 +11,19 @@ use tokio::sync::mpsc;
use winreg::enums::*; use winreg::enums::*;
use winreg::RegKey; use winreg::RegKey;
use crate::domain::models::{InstallProgress, InstallTask}; use crate::domain::models::{InstallProgress, InstallTask, TaskEventPayload};
use crate::services::essentials_service; use crate::services::essentials_service;
use crate::services::log_service::emit_log; use crate::services::log_service::emit_log;
use crate::winget::PostInstallStep; use crate::winget::PostInstallStep;
pub struct AppState { pub struct AppState {
pub install_tx: mpsc::Sender<InstallTask>, pub install_tx: mpsc::Sender<InstallTask>,
pub app_handle: AppHandle,
} }
pub fn create_install_state(handle: AppHandle) -> AppState { pub fn create_install_state(handle: AppHandle) -> AppState {
let (tx, mut rx) = mpsc::channel::<InstallTask>(100); let (tx, mut rx) = mpsc::channel::<InstallTask>(100);
let runtime_handle = handle.clone();
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
let perc_re = Regex::new(r"(\d+)\s*%").unwrap(); 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 enable_post_install_flag = task.enable_post_install;
let log_id = format!("install-{}", task_id); let log_id = format!("install-{}", task_id);
emit_task_event(
let _ = handle.emit( &runtime_handle,
"install-status", &log_id,
InstallProgress { &task_id,
id: task_id.clone(), "install",
status: "installing".to_string(), "running",
progress: 0.0, "installing",
}, 0.0,
task_version.clone(),
None,
); );
let mut args = vec!["install".to_string()]; 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() { if use_manifest && manifest_url.is_some() {
let url = manifest_url.unwrap(); let url = manifest_url.unwrap();
display_cmd = format!("Winget Install (Manifest): {} from {}", task_id, url); 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( emit_log(
&handle, &runtime_handle,
&log_id, &log_id,
&display_cmd, &display_cmd,
"Downloading remote manifest...", "Downloading remote manifest...",
@@ -85,19 +100,22 @@ pub fn create_install_state(handle: AppHandle) -> AppState {
if temp_manifest_path.is_none() { if temp_manifest_path.is_none() {
emit_log( emit_log(
&handle, &runtime_handle,
&log_id, &log_id,
"Error", "Error",
"Failed to download or save manifest.", "Failed to download or save manifest.",
"error", "error",
); );
let _ = handle.emit( emit_task_event(
"install-status", &runtime_handle,
InstallProgress { &log_id,
id: task_id.clone(), &task_id,
status: "error".to_string(), "install",
progress: 0.0, "failed",
}, "manifest_error",
0.0,
task_version.clone(),
Some("Failed to download or save manifest".to_string()),
); );
continue; continue;
} }
@@ -128,12 +146,23 @@ pub fn create_install_state(handle: AppHandle) -> AppState {
let full_command = format!("winget {}", args.join(" ")); let full_command = format!("winget {}", args.join(" "));
emit_log( emit_log(
&handle, &runtime_handle,
&log_id, &log_id,
&display_cmd, &display_cmd,
&format!("Executing: {}\n---", full_command), &format!("Executing: {}\n---", full_command),
"info", "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") let child = Command::new("winget")
.args(&args) .args(&args)
@@ -147,7 +176,7 @@ pub fn create_install_state(handle: AppHandle) -> AppState {
let stdout_handle = child_proc.stdout.take().map(|stdout| { let stdout_handle = child_proc.stdout.take().map(|stdout| {
spawn_install_stream_reader( spawn_install_stream_reader(
stdout, stdout,
handle.clone(), runtime_handle.clone(),
log_id.clone(), log_id.clone(),
task_id.clone(), task_id.clone(),
"stdout", "stdout",
@@ -158,7 +187,7 @@ pub fn create_install_state(handle: AppHandle) -> AppState {
let stderr_handle = child_proc.stderr.take().map(|stderr| { let stderr_handle = child_proc.stderr.take().map(|stderr| {
spawn_install_stream_reader( spawn_install_stream_reader(
stderr, stderr,
handle.clone(), runtime_handle.clone(),
log_id.clone(), log_id.clone(),
task_id.clone(), task_id.clone(),
"stderr", "stderr",
@@ -177,7 +206,7 @@ pub fn create_install_state(handle: AppHandle) -> AppState {
let status_result = if exit_status { "success" } else { "error" }; let status_result = if exit_status { "success" } else { "error" };
if status_result == "success" && enable_post_install_flag { 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)); .and_then(|repo| repo.essentials.into_iter().find(|s| s.id == task_id));
if let Some(sw) = software_info { 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 { } else if let Some(url) = sw.post_install_url {
emit_log( emit_log(
&handle, &runtime_handle,
&log_id, &log_id,
"Post-Install", "Post-Install",
"Local config not found, fetching remote config...", "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) { match serde_json::from_str::<Vec<PostInstallStep>>(&text) {
Ok(steps) => { Ok(steps) => {
emit_log( emit_log(
&handle, &runtime_handle,
&log_id, &log_id,
"Post-Install", "Post-Install",
&format!( &format!(
@@ -219,7 +248,7 @@ pub fn create_install_state(handle: AppHandle) -> AppState {
} }
Err(e) => { Err(e) => {
emit_log( emit_log(
&handle, &runtime_handle,
&log_id, &log_id,
"Post-Install Error", "Post-Install Error",
&format!("JSON Parse Error: {}. Raw Content: {}", e, text), &format!("JSON Parse Error: {}. Raw Content: {}", e, text),
@@ -230,7 +259,7 @@ pub fn create_install_state(handle: AppHandle) -> AppState {
} }
} else { } else {
emit_log( emit_log(
&handle, &runtime_handle,
&log_id, &log_id,
"Post-Install Error", "Post-Install Error",
&format!("Remote config HTTP Error: {}", resp.status()), &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 { if let Some(steps) = final_steps {
let _ = handle.emit( emit_task_event(
"install-status", &runtime_handle,
InstallProgress { &log_id,
id: task_id.clone(), &task_id,
status: "configuring".to_string(), "install",
progress: 1.0, "running",
}, "configuring",
1.0,
task_version.clone(),
Some("Starting post-installation configuration".to_string()),
); );
emit_log( emit_log(
&handle, &runtime_handle,
&log_id, &log_id,
"Post-Install", "Post-Install",
"Starting post-installation configuration...", "Starting post-installation configuration...",
"info", "info",
); );
if let Err(e) = execute_post_install(&handle, &log_id, steps).await { if let Err(e) = execute_post_install(&runtime_handle, &log_id, steps).await {
emit_log(&handle, &log_id, "Post-Install Error", &e, "error"); emit_log(&runtime_handle, &log_id, "Post-Install Error", &e, "error");
} else { } else {
emit_log( emit_log(
&handle, &runtime_handle,
&log_id, &log_id,
"Post-Install", "Post-Install",
"Post-installation configuration completed.", "Post-installation configuration completed.",
@@ -273,21 +305,35 @@ pub fn create_install_state(handle: AppHandle) -> AppState {
status_result status_result
} }
Err(e) => { 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" "error"
} }
}; };
let _ = handle.emit( emit_task_event(
"install-status", &runtime_handle,
InstallProgress { &log_id,
id: task_id.clone(), &task_id,
status: status_result.to_string(), "install",
progress: 1.0, if status_result == "success" { "completed" } else { "failed" },
}, status_result,
1.0,
task_version.clone(),
Some(format!("Execution finished: {}", status_result)),
); );
emit_log( emit_log(
&handle, &runtime_handle,
&log_id, &log_id,
"Result", "Result",
&format!("Execution finished: {}", status_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] #[tauri::command]
@@ -312,6 +358,18 @@ pub async fn install_software(
task: InstallTask, task: InstallTask,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<(), String> { ) -> 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()) 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, 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; is_progress = true;
} }
} else if let Some(caps) = size_re.captures(clean_line) { } 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), 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; 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 { fn expand_win_path(path: &str) -> PathBuf {
let mut expanded = path.to_string(); let mut expanded = path.to_string();
let env_vars = [ let env_vars = [

View File

@@ -3,11 +3,11 @@ import { invoke } from '@tauri-apps/api/core'
import { listen } from '@tauri-apps/api/event' import { listen } from '@tauri-apps/api/event'
import { useCatalogStore } from './catalog' import { useCatalogStore } from './catalog'
import type { ActiveTaskState, LogEntry } from './types' import type { ActiveTaskState, LogEntry, TaskEventPayload, TaskRecord } from './types'
export const useTaskRuntimeStore = defineStore('task-runtime', { export const useTaskRuntimeStore = defineStore('task-runtime', {
state: () => ({ state: () => ({
activeTasks: {} as Record<string, ActiveTaskState>, taskRecords: {} as Record<string, TaskRecord>,
selectedEssentialIds: [] as string[], selectedEssentialIds: [] as string[],
selectedUpdateIds: [] as string[], selectedUpdateIds: [] as string[],
logs: [] as LogEntry[], logs: [] as LogEntry[],
@@ -16,9 +16,21 @@ export const useTaskRuntimeStore = defineStore('task-runtime', {
postInstallPrefs: {} as Record<string, boolean> postInstallPrefs: {} as Record<string, boolean>
}), }),
getters: { getters: {
isTaskBusy: (state) => Object.values(state.activeTasks).some(task => activeTasks: (state): Record<string, ActiveTaskState> => {
task.status === 'pending' || task.status === 'installing' 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: { actions: {
toggleSelection(id: string, type: 'essential' | 'update') { toggleSelection(id: string, type: 'essential' | 'update') {
@@ -48,7 +60,6 @@ export const useTaskRuntimeStore = defineStore('task-runtime', {
if (!software) return if (!software) return
const enablePostInstall = this.postInstallPrefs[id] !== false const enablePostInstall = this.postInstallPrefs[id] !== false
this.activeTasks[id] = { status: 'pending', progress: 0, targetVersion }
try { try {
await invoke('install_software', { await invoke('install_software', {
task: { task: {
@@ -61,7 +72,16 @@ export const useTaskRuntimeStore = defineStore('task-runtime', {
}) })
} catch (err) { } catch (err) {
console.error('Invoke install failed:', 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 () => { this.refreshTimer = setTimeout(async () => {
await catalog.fetchAllData() await catalog.fetchAllData()
Object.keys(this.activeTasks).forEach(id => { Object.keys(this.taskRecords).forEach(taskId => {
const status = this.activeTasks[id].status const status = this.taskRecords[taskId].status
if (status === 'success' || status === 'error') { if (status === 'completed' || status === 'failed') {
delete this.activeTasks[id] delete this.taskRecords[taskId]
} }
}) })
this.refreshTimer = null 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 if ((window as Window & { __tauri_listener_init?: boolean }).__tauri_listener_init) return
;(window as Window & { __tauri_listener_init?: boolean }).__tauri_listener_init = true ;(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 catalog = useCatalogStore()
const { id, status, progress } = event.payload const payload = event.payload
const task = this.activeTasks[id] 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 (payload.status === 'completed' || payload.status === 'failed') {
if (status === 'success') { if (payload.status === 'completed') {
try { 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) { 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) { if (index !== -1) {
catalog.allSoftware[index] = { ...catalog.allSoftware[index], ...latestInfo } catalog.allSoftware[index] = { ...catalog.allSoftware[index], ...latestInfo }
} else { } else {
@@ -108,17 +137,17 @@ export const useTaskRuntimeStore = defineStore('task-runtime', {
console.error('Partial refresh failed:', err) console.error('Partial refresh failed:', err)
} }
this.selectedEssentialIds = this.selectedEssentialIds.filter(item => item !== id) this.selectedEssentialIds = this.selectedEssentialIds.filter(item => item !== payload.software_id)
this.selectedUpdateIds = this.selectedUpdateIds.filter(item => item !== id) this.selectedUpdateIds = this.selectedUpdateIds.filter(item => item !== payload.software_id)
setTimeout(() => { setTimeout(() => {
if (this.activeTasks[id]?.status === 'success') { if (this.taskRecords[payload.task_id]?.status === 'completed') {
delete this.activeTasks[id] delete this.taskRecords[payload.task_id]
} }
}, 3000) }, 3000)
} }
const index = this.batchQueue.indexOf(id) const index = this.batchQueue.indexOf(payload.software_id)
if (index !== -1) { if (index !== -1) {
this.batchQueue.splice(index, 1) this.batchQueue.splice(index, 1)
if (this.batchQueue.length === 0) { 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 }) => { listen('log-event', (event: { payload: LogEntry }) => {
const payload = event.payload const payload = event.payload
const existingLog = this.logs.find(item => item.id === payload.id) 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'
}

View File

@@ -47,6 +47,28 @@ export interface ActiveTaskState {
targetVersion?: string 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 { export interface AppSettings {
repo_url: string repo_url: string
} }