From a0aa00f6ba260d0e8c64993e87e780840a3a5ac0 Mon Sep 17 00:00:00 2001 From: Louis Knight-Webb Date: Mon, 16 Jun 2025 20:45:34 -0400 Subject: [PATCH] Stop execution --- backend/src/executors/echo.rs | 5 +- backend/src/lib.rs | 1 + backend/src/routes/tasks.rs | 81 ++++++++++++++++ .../components/tasks/TaskDetailsDialog.tsx | 95 ++++++++++++++----- 4 files changed, 156 insertions(+), 26 deletions(-) diff --git a/backend/src/executors/echo.rs b/backend/src/executors/echo.rs index ac556f59..153518ba 100644 --- a/backend/src/executors/echo.rs +++ b/backend/src/executors/echo.rs @@ -34,13 +34,12 @@ impl Executor for EchoExecutor { // For demonstration of streaming, we can use a shell command that outputs multiple lines let script = format!( r#"echo "Starting task: {}" -for i in {{1..5}}; do +for i in {{1..50}}; do echo "Progress line $i" sleep 1 done echo "Task completed: {}""#, - task.title, - task.title + task.title, task.title ); let child = Command::new("sh") diff --git a/backend/src/lib.rs b/backend/src/lib.rs index 1c31dcd2..03ffc599 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -1,4 +1,5 @@ pub mod auth; +pub mod execution_monitor; pub mod executor; pub mod executors; pub mod models; diff --git a/backend/src/routes/tasks.rs b/backend/src/routes/tasks.rs index 65b90e08..0f8c75a0 100644 --- a/backend/src/routes/tasks.rs +++ b/backend/src/routes/tasks.rs @@ -281,6 +281,86 @@ pub async fn create_task_attempt_activity( } } +pub async fn stop_task_attempt( + _auth: AuthUser, + Path((project_id, task_id, attempt_id)): Path<(Uuid, Uuid, Uuid)>, + Extension(pool): Extension, + Extension(app_state): Extension +) -> Result>, StatusCode> { + // Verify task attempt exists and belongs to the correct task + match TaskAttempt::exists_for_task(&pool, attempt_id, task_id, project_id).await { + Ok(false) => return Err(StatusCode::NOT_FOUND), + Err(e) => { + tracing::error!("Failed to check task attempt existence: {}", e); + return Err(StatusCode::INTERNAL_SERVER_ERROR); + } + Ok(true) => {} + } + + // Find and stop the running execution + let mut stopped = false; + { + let mut executions = app_state.running_executions.lock().await; + let mut execution_id_to_remove = None; + + // Find the execution for this attempt + for (exec_id, execution) in executions.iter_mut() { + if execution.task_attempt_id == attempt_id { + // Kill the process + match execution.child.kill().await { + Ok(_) => { + stopped = true; + execution_id_to_remove = Some(*exec_id); + tracing::info!("Stopped execution for task attempt {}", attempt_id); + break; + } + Err(e) => { + tracing::error!("Failed to kill process for attempt {}: {}", attempt_id, e); + return Err(StatusCode::INTERNAL_SERVER_ERROR); + } + } + } + } + + // Remove the stopped execution from the map + if let Some(exec_id) = execution_id_to_remove { + executions.remove(&exec_id); + } + } + + if !stopped { + return Ok(ResponseJson(ApiResponse { + success: true, + data: None, + message: Some("No running execution found for this attempt".to_string()), + })); + } + + // Create a new activity record to mark as stopped + let activity_id = Uuid::new_v4(); + let create_activity = CreateTaskAttemptActivity { + task_attempt_id: attempt_id, + status: Some(TaskAttemptStatus::Paused), + note: Some("Execution stopped by user".to_string()), + }; + + if let Err(e) = TaskAttemptActivity::create( + &pool, + &create_activity, + activity_id, + TaskAttemptStatus::Paused, + ).await { + tracing::error!("Failed to create stopped activity: {}", e); + return Err(StatusCode::INTERNAL_SERVER_ERROR); + } + + Ok(ResponseJson(ApiResponse { + success: true, + data: None, + message: Some("Task attempt stopped successfully".to_string()), + })) +} + pub fn tasks_router() -> Router { use axum::routing::{post, put, delete}; @@ -289,6 +369,7 @@ pub fn tasks_router() -> Router { .route("/projects/:project_id/tasks/:task_id", get(get_task).put(update_task).delete(delete_task)) .route("/projects/:project_id/tasks/:task_id/attempts", get(get_task_attempts).post(create_task_attempt)) .route("/projects/:project_id/tasks/:task_id/attempts/:attempt_id/activities", get(get_task_attempt_activities).post(create_task_attempt_activity)) + .route("/projects/:project_id/tasks/:task_id/attempts/:attempt_id/stop", post(stop_task_attempt)) } #[cfg(test)] diff --git a/frontend/src/components/tasks/TaskDetailsDialog.tsx b/frontend/src/components/tasks/TaskDetailsDialog.tsx index f21bccb0..2221f46c 100644 --- a/frontend/src/components/tasks/TaskDetailsDialog.tsx +++ b/frontend/src/components/tasks/TaskDetailsDialog.tsx @@ -75,6 +75,7 @@ export function TaskDetailsDialog({ const [activitiesLoading, setActivitiesLoading] = useState(false); const [selectedExecutor, setSelectedExecutor] = useState("echo"); const [creatingAttempt, setCreatingAttempt] = useState(false); + const [stoppingAttempt, setStoppingAttempt] = useState(false); // Edit mode state const [isEditMode, setIsEditMode] = useState(false); @@ -83,6 +84,10 @@ export function TaskDetailsDialog({ const [editedStatus, setEditedStatus] = useState("todo"); const [savingTask, setSavingTask] = useState(false); + // Check if the selected attempt is currently running (latest activity is "inprogress") + const isAttemptRunning = selectedAttempt && attemptActivities.length > 0 && + attemptActivities[0].status === "inprogress"; + useEffect(() => { if (isOpen && task) { fetchTaskAttempts(task.id); @@ -236,6 +241,34 @@ export function TaskDetailsDialog({ } }; + const stopTaskAttempt = async () => { + if (!task || !selectedAttempt) return; + + try { + setStoppingAttempt(true); + const response = await makeAuthenticatedRequest( + `/api/projects/${projectId}/tasks/${task.id}/attempts/${selectedAttempt.id}/stop`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + } + ); + + if (response.ok) { + // Refresh the activities list to show the stopped status + fetchAttemptActivities(selectedAttempt.id); + } else { + onError("Failed to stop task attempt"); + } + } catch (err) { + onError("Failed to stop task attempt"); + } finally { + setStoppingAttempt(false); + } + }; + return ( @@ -513,31 +546,47 @@ export function TaskDetailsDialog({
- - + {isAttemptRunning && ( + + )} +
+ + + +