parent
906c44ef22
commit
467aebc0af
8 changed files with 376 additions and 23 deletions
|
|
@ -3,6 +3,7 @@ use crate::models::ticket::ProcessedTicket;
|
||||||
use crate::models::worktree::Worktree;
|
use crate::models::worktree::Worktree;
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use tauri::async_runtime;
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
|
@ -63,9 +64,22 @@ pub fn retry_ticket(state: State<'_, AppState>, ticket_id: String) -> Result<(),
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn cancel_ticket(state: State<'_, AppState>, ticket_id: String) -> Result<(), AppError> {
|
pub fn cancel_ticket(state: State<'_, AppState>, ticket_id: String) -> Result<(), AppError> {
|
||||||
|
{
|
||||||
|
let conn = state.db.lock().map_err(|e| AppError::from(e.to_string()))?;
|
||||||
|
let ticket = ProcessedTicket::get_by_id(&conn, &ticket_id)?;
|
||||||
|
|
||||||
|
if ticket.status == "Done" || ticket.status == "Cancelled" {
|
||||||
|
return Err(AppError::from(format!(
|
||||||
|
"Cannot cancel ticket with status '{}'",
|
||||||
|
ticket.status
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async_runtime::block_on(state.process_registry.cancel_ticket(&ticket_id));
|
||||||
|
|
||||||
let conn = state.db.lock().map_err(|e| AppError::from(e.to_string()))?;
|
let conn = state.db.lock().map_err(|e| AppError::from(e.to_string()))?;
|
||||||
let ticket = ProcessedTicket::get_by_id(&conn, &ticket_id)?;
|
let ticket = ProcessedTicket::get_by_id(&conn, &ticket_id)?;
|
||||||
|
|
||||||
if ticket.status == "Done" || ticket.status == "Cancelled" {
|
if ticket.status == "Done" || ticket.status == "Cancelled" {
|
||||||
return Err(AppError::from(format!(
|
return Err(AppError::from(format!(
|
||||||
"Cannot cancel ticket with status '{}'",
|
"Cannot cancel ticket with status '{}'",
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use crate::models::agent_task::AgentTask;
|
||||||
use crate::models::module::{ProjectModule, MODULE_AGENT_TASK_RUNNER};
|
use crate::models::module::{ProjectModule, MODULE_AGENT_TASK_RUNNER};
|
||||||
use crate::models::project::Project;
|
use crate::models::project::Project;
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
use tauri::async_runtime;
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|
@ -87,11 +88,27 @@ pub fn retry_agent_task(state: State<'_, AppState>, task_id: String) -> Result<(
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn cancel_agent_task(state: State<'_, AppState>, task_id: String) -> Result<(), AppError> {
|
pub fn cancel_agent_task(state: State<'_, AppState>, task_id: String) -> Result<(), AppError> {
|
||||||
|
{
|
||||||
|
let db = state
|
||||||
|
.db
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| AppError::from(format!("Database lock failed: {}", e)))?;
|
||||||
|
|
||||||
|
let task = AgentTask::get_by_id(&db, &task_id)?;
|
||||||
|
if task.status == "done" || task.status == "cancelled" {
|
||||||
|
return Err(AppError::from(format!(
|
||||||
|
"Impossible d'annuler une tâche avec le statut '{}'",
|
||||||
|
task.status
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async_runtime::block_on(state.process_registry.cancel_task(&task_id));
|
||||||
|
|
||||||
let db = state
|
let db = state
|
||||||
.db
|
.db
|
||||||
.lock()
|
.lock()
|
||||||
.map_err(|e| AppError::from(format!("Database lock failed: {}", e)))?;
|
.map_err(|e| AppError::from(format!("Database lock failed: {}", e)))?;
|
||||||
|
|
||||||
let task = AgentTask::get_by_id(&db, &task_id)?;
|
let task = AgentTask::get_by_id(&db, &task_id)?;
|
||||||
if task.status == "done" || task.status == "cancelled" {
|
if task.status == "done" || task.status == "cancelled" {
|
||||||
return Err(AppError::from(format!(
|
return Err(AppError::from(format!(
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ pub struct AppState {
|
||||||
pub db: Arc<Mutex<rusqlite::Connection>>,
|
pub db: Arc<Mutex<rusqlite::Connection>>,
|
||||||
pub encryption_key: [u8; 32],
|
pub encryption_key: [u8; 32],
|
||||||
pub http_client: reqwest::Client,
|
pub http_client: reqwest::Client,
|
||||||
|
pub process_registry: services::process_registry::ProcessRegistry,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
|
@ -29,12 +30,14 @@ pub fn run() {
|
||||||
let encryption_key = load_or_generate_key(&key_path)?;
|
let encryption_key = load_or_generate_key(&key_path)?;
|
||||||
|
|
||||||
let http_client = reqwest::Client::new();
|
let http_client = reqwest::Client::new();
|
||||||
|
let process_registry = services::process_registry::ProcessRegistry::default();
|
||||||
|
|
||||||
let db_arc = Arc::new(Mutex::new(conn));
|
let db_arc = Arc::new(Mutex::new(conn));
|
||||||
app.manage(AppState {
|
app.manage(AppState {
|
||||||
db: db_arc.clone(),
|
db: db_arc.clone(),
|
||||||
encryption_key,
|
encryption_key,
|
||||||
http_client: http_client.clone(),
|
http_client: http_client.clone(),
|
||||||
|
process_registry: process_registry.clone(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start background poller
|
// Start background poller
|
||||||
|
|
@ -46,10 +49,14 @@ pub fn run() {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Start agent orchestrator
|
// Start agent orchestrator
|
||||||
services::orchestrator::start(db_arc.clone(), app.handle().clone());
|
services::orchestrator::start(
|
||||||
|
db_arc.clone(),
|
||||||
|
app.handle().clone(),
|
||||||
|
process_registry.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
// Start agent task runner
|
// Start agent task runner
|
||||||
services::task_runner::start(db_arc, app.handle().clone());
|
services::task_runner::start(db_arc, app.handle().clone(), process_registry);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
|
use crate::services::process_registry::ProcessRegistry;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
|
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
|
use tokio::sync::Mutex as AsyncMutex;
|
||||||
use tokio::time::{timeout, Duration};
|
use tokio::time::{timeout, Duration};
|
||||||
|
|
||||||
fn normalize_process_stderr(stderr: &str) -> String {
|
fn normalize_process_stderr(stderr: &str) -> String {
|
||||||
|
|
@ -60,6 +64,128 @@ pub async fn run_agent_command(
|
||||||
Ok(stdout)
|
Ok(stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn run_agent_command_for_task(
|
||||||
|
command: &str,
|
||||||
|
args: &[String],
|
||||||
|
prompt: &str,
|
||||||
|
working_dir: &str,
|
||||||
|
timeout_secs: u64,
|
||||||
|
process_registry: &ProcessRegistry,
|
||||||
|
task_id: &str,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let child = Command::new(command)
|
||||||
|
.args(args)
|
||||||
|
.stdin(std::process::Stdio::piped())
|
||||||
|
.stdout(std::process::Stdio::piped())
|
||||||
|
.stderr(std::process::Stdio::piped())
|
||||||
|
.current_dir(working_dir)
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| format!("Failed to spawn '{}': {}", command, e))?;
|
||||||
|
let child = Arc::new(AsyncMutex::new(child));
|
||||||
|
let cancellation_requested = Arc::new(AtomicBool::new(false));
|
||||||
|
process_registry.register_task(task_id, child.clone(), cancellation_requested.clone());
|
||||||
|
|
||||||
|
let (stdin, stdout, stderr) = {
|
||||||
|
let mut child_guard = child.lock().await;
|
||||||
|
let stdin = child_guard.stdin.take();
|
||||||
|
let stdout = child_guard.stdout.take();
|
||||||
|
let stderr = child_guard.stderr.take();
|
||||||
|
(stdin, stdout, stderr)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(mut stdin) = stdin {
|
||||||
|
stdin.write_all(prompt.as_bytes()).await.map_err(|e| {
|
||||||
|
process_registry.unregister_task(task_id);
|
||||||
|
format!("Failed to write prompt to stdin: {}", e)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = stdout.ok_or_else(|| {
|
||||||
|
process_registry.unregister_task(task_id);
|
||||||
|
"Failed to capture stdout".to_string()
|
||||||
|
})?;
|
||||||
|
let stderr = stderr.ok_or_else(|| {
|
||||||
|
process_registry.unregister_task(task_id);
|
||||||
|
"Failed to capture stderr".to_string()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let stderr_task = tokio::spawn(async move {
|
||||||
|
let mut stderr_reader = BufReader::new(stderr);
|
||||||
|
let mut stderr_output = String::new();
|
||||||
|
stderr_reader
|
||||||
|
.read_to_string(&mut stderr_output)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to read stderr: {}", e))?;
|
||||||
|
Ok::<String, String>(stderr_output)
|
||||||
|
});
|
||||||
|
|
||||||
|
let read_future = async {
|
||||||
|
let mut reader = BufReader::new(stdout).lines();
|
||||||
|
let mut output = String::new();
|
||||||
|
|
||||||
|
while let Ok(Some(line)) = reader.next_line().await {
|
||||||
|
output.push_str(&line);
|
||||||
|
output.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = {
|
||||||
|
let mut child_guard = child.lock().await;
|
||||||
|
child_guard
|
||||||
|
.wait()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to wait for process: {}", e))
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let stderr_output = stderr_task
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to join stderr reader: {}", e))??;
|
||||||
|
|
||||||
|
Ok::<(String, String, std::process::ExitStatus), String>((output, stderr_output, status))
|
||||||
|
};
|
||||||
|
|
||||||
|
let (output, stderr_output, status) =
|
||||||
|
match timeout(Duration::from_secs(timeout_secs), read_future).await {
|
||||||
|
Ok(result) => match result {
|
||||||
|
Ok(values) => values,
|
||||||
|
Err(e) => {
|
||||||
|
process_registry.unregister_task(task_id);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(_) => {
|
||||||
|
cancellation_requested.store(true, Ordering::SeqCst);
|
||||||
|
{
|
||||||
|
let mut child_guard = child.lock().await;
|
||||||
|
let _ = child_guard.start_kill();
|
||||||
|
}
|
||||||
|
process_registry.unregister_task(task_id);
|
||||||
|
return Err(format!("CLI command timed out after {}s", timeout_secs));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
process_registry.unregister_task(task_id);
|
||||||
|
|
||||||
|
if cancellation_requested.load(Ordering::SeqCst) {
|
||||||
|
return Err("CLI command cancelled".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
let stderr = normalize_process_stderr(&stderr_output);
|
||||||
|
let code = status.code().unwrap_or(-1);
|
||||||
|
if stderr.is_empty() {
|
||||||
|
return Err(format!("CLI command exited with code {}", code));
|
||||||
|
}
|
||||||
|
return Err(format!("CLI command exited with code {}: {}", code, stderr));
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = output.trim().to_string();
|
||||||
|
if stdout.is_empty() {
|
||||||
|
return Ok("(empty response)".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(stdout)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn run_agent_command_streaming<F>(
|
pub async fn run_agent_command_streaming<F>(
|
||||||
command: &str,
|
command: &str,
|
||||||
args: &[String],
|
args: &[String],
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ pub mod filter_engine;
|
||||||
pub mod notifier;
|
pub mod notifier;
|
||||||
pub mod orchestrator;
|
pub mod orchestrator;
|
||||||
pub mod poller;
|
pub mod poller;
|
||||||
|
pub mod process_registry;
|
||||||
pub mod task_runner;
|
pub mod task_runner;
|
||||||
pub mod tuleap_client;
|
pub mod tuleap_client;
|
||||||
pub mod worktree_manager;
|
pub mod worktree_manager;
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,15 @@ use crate::models::project::Project;
|
||||||
use crate::models::ticket::ProcessedTicket;
|
use crate::models::ticket::ProcessedTicket;
|
||||||
use crate::models::tracker::WatchedTracker;
|
use crate::models::tracker::WatchedTracker;
|
||||||
use crate::models::worktree::Worktree;
|
use crate::models::worktree::Worktree;
|
||||||
|
use crate::services::process_registry::ProcessRegistry;
|
||||||
use crate::services::{notifier, worktree_manager};
|
use crate::services::{notifier, worktree_manager};
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use tauri::{AppHandle, Emitter};
|
use tauri::{AppHandle, Emitter};
|
||||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
|
use tokio::sync::Mutex as AsyncMutex;
|
||||||
use tokio::time::{interval, timeout, Duration};
|
use tokio::time::{interval, timeout, Duration};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
|
@ -142,8 +145,9 @@ pub async fn run_cli_command(
|
||||||
timeout_secs: u64,
|
timeout_secs: u64,
|
||||||
app_handle: &AppHandle,
|
app_handle: &AppHandle,
|
||||||
ticket_id: &str,
|
ticket_id: &str,
|
||||||
|
process_registry: &ProcessRegistry,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let mut child = Command::new(command)
|
let child = Command::new(command)
|
||||||
.args(args)
|
.args(args)
|
||||||
.stdin(std::process::Stdio::piped())
|
.stdin(std::process::Stdio::piped())
|
||||||
.stdout(std::process::Stdio::piped())
|
.stdout(std::process::Stdio::piped())
|
||||||
|
|
@ -151,16 +155,29 @@ pub async fn run_cli_command(
|
||||||
.current_dir(working_dir)
|
.current_dir(working_dir)
|
||||||
.spawn()
|
.spawn()
|
||||||
.map_err(|e| format!("Failed to spawn '{}': {}", command, e))?;
|
.map_err(|e| format!("Failed to spawn '{}': {}", command, e))?;
|
||||||
|
let child = Arc::new(AsyncMutex::new(child));
|
||||||
|
let cancellation_requested = Arc::new(AtomicBool::new(false));
|
||||||
|
process_registry.register_ticket(ticket_id, child.clone(), cancellation_requested.clone());
|
||||||
|
|
||||||
if let Some(mut stdin) = child.stdin.take() {
|
let (stdin, stdout) = {
|
||||||
|
let mut child_guard = child.lock().await;
|
||||||
|
let stdin = child_guard.stdin.take();
|
||||||
|
let stdout = child_guard.stdout.take();
|
||||||
|
(stdin, stdout)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(mut stdin) = stdin {
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
stdin
|
stdin.write_all(prompt.as_bytes()).await.map_err(|e| {
|
||||||
.write_all(prompt.as_bytes())
|
process_registry.unregister_ticket(ticket_id);
|
||||||
.await
|
format!("Failed to write to stdin: {}", e)
|
||||||
.map_err(|e| format!("Failed to write to stdin: {}", e))?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let stdout = child.stdout.take().ok_or("Failed to capture stdout")?;
|
let stdout = stdout.ok_or_else(|| {
|
||||||
|
process_registry.unregister_ticket(ticket_id);
|
||||||
|
"Failed to capture stdout".to_string()
|
||||||
|
})?;
|
||||||
let mut reader = BufReader::new(stdout).lines();
|
let mut reader = BufReader::new(stdout).lines();
|
||||||
let mut output = String::new();
|
let mut output = String::new();
|
||||||
|
|
||||||
|
|
@ -179,14 +196,29 @@ pub async fn run_cli_command(
|
||||||
output
|
output
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = timeout(Duration::from_secs(timeout_secs), read_future)
|
let result = match timeout(Duration::from_secs(timeout_secs), read_future).await {
|
||||||
.await
|
Ok(output) => output,
|
||||||
.map_err(|_| format!("CLI command timed out after {}s", timeout_secs))?;
|
Err(_) => {
|
||||||
|
cancellation_requested.store(true, Ordering::SeqCst);
|
||||||
|
{
|
||||||
|
let mut child_guard = child.lock().await;
|
||||||
|
let _ = child_guard.start_kill();
|
||||||
|
}
|
||||||
|
process_registry.unregister_ticket(ticket_id);
|
||||||
|
return Err(format!("CLI command timed out after {}s", timeout_secs));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let status = child
|
let wait_result = {
|
||||||
.wait()
|
let mut child_guard = child.lock().await;
|
||||||
.await
|
child_guard.wait().await
|
||||||
.map_err(|e| format!("Failed to wait for process: {}", e))?;
|
};
|
||||||
|
process_registry.unregister_ticket(ticket_id);
|
||||||
|
let status = wait_result.map_err(|e| format!("Failed to wait for process: {}", e))?;
|
||||||
|
|
||||||
|
if cancellation_requested.load(Ordering::SeqCst) {
|
||||||
|
return Err("CLI command cancelled".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
let code = status.code().unwrap_or(-1);
|
let code = status.code().unwrap_or(-1);
|
||||||
|
|
@ -196,9 +228,17 @@ pub async fn run_cli_command(
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_ticket_cancelled(db: &Arc<Mutex<Connection>>, ticket_id: &str) -> Result<bool, String> {
|
||||||
|
let conn = db.lock().map_err(|e| format!("DB lock failed: {}", e))?;
|
||||||
|
let current =
|
||||||
|
ProcessedTicket::get_by_id(&conn, ticket_id).map_err(|e| format!("get_by_id: {}", e))?;
|
||||||
|
Ok(current.status == "Cancelled")
|
||||||
|
}
|
||||||
|
|
||||||
async fn process_ticket(
|
async fn process_ticket(
|
||||||
db: &Arc<Mutex<Connection>>,
|
db: &Arc<Mutex<Connection>>,
|
||||||
app_handle: &AppHandle,
|
app_handle: &AppHandle,
|
||||||
|
process_registry: &ProcessRegistry,
|
||||||
) -> Result<bool, String> {
|
) -> Result<bool, String> {
|
||||||
let (ticket, tracker, project) = {
|
let (ticket, tracker, project) = {
|
||||||
let conn = db.lock().map_err(|e| format!("DB lock failed: {}", e))?;
|
let conn = db.lock().map_err(|e| format!("DB lock failed: {}", e))?;
|
||||||
|
|
@ -361,12 +401,16 @@ async fn process_ticket(
|
||||||
600,
|
600,
|
||||||
app_handle,
|
app_handle,
|
||||||
&ticket.id,
|
&ticket.id,
|
||||||
|
process_registry,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let analyst_report = match analyst_result {
|
let analyst_report = match analyst_result {
|
||||||
Ok(report) => report,
|
Ok(report) => report,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
if is_ticket_cancelled(db, &ticket.id)? {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
record_ticket_error(
|
record_ticket_error(
|
||||||
db,
|
db,
|
||||||
app_handle,
|
app_handle,
|
||||||
|
|
@ -379,6 +423,10 @@ async fn process_ticket(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if is_ticket_cancelled(db, &ticket.id)? {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let conn = db.lock().map_err(|e| format!("DB lock: {}", e))?;
|
let conn = db.lock().map_err(|e| format!("DB lock: {}", e))?;
|
||||||
ProcessedTicket::set_analyst_report(&conn, &ticket.id, &analyst_report)
|
ProcessedTicket::set_analyst_report(&conn, &ticket.id, &analyst_report)
|
||||||
|
|
@ -387,6 +435,10 @@ async fn process_ticket(
|
||||||
|
|
||||||
let verdict = parse_verdict(&analyst_report);
|
let verdict = parse_verdict(&analyst_report);
|
||||||
if verdict == Verdict::NoFix {
|
if verdict == Verdict::NoFix {
|
||||||
|
if is_ticket_cancelled(db, &ticket.id)? {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
let conn = db.lock().map_err(|e| format!("DB lock: {}", e))?;
|
let conn = db.lock().map_err(|e| format!("DB lock: {}", e))?;
|
||||||
ProcessedTicket::update_status(&conn, &ticket.id, "Done")
|
ProcessedTicket::update_status(&conn, &ticket.id, "Done")
|
||||||
.map_err(|e| format!("update_status: {}", e))?;
|
.map_err(|e| format!("update_status: {}", e))?;
|
||||||
|
|
@ -460,12 +512,16 @@ async fn process_ticket(
|
||||||
600,
|
600,
|
||||||
app_handle,
|
app_handle,
|
||||||
&ticket.id,
|
&ticket.id,
|
||||||
|
process_registry,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let developer_report = match developer_result {
|
let developer_report = match developer_result {
|
||||||
Ok(report) => report,
|
Ok(report) => report,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
if is_ticket_cancelled(db, &ticket.id)? {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
record_ticket_error(
|
record_ticket_error(
|
||||||
db,
|
db,
|
||||||
app_handle,
|
app_handle,
|
||||||
|
|
@ -478,6 +534,10 @@ async fn process_ticket(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if is_ticket_cancelled(db, &ticket.id)? {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let conn = db.lock().map_err(|e| format!("DB lock: {}", e))?;
|
let conn = db.lock().map_err(|e| format!("DB lock: {}", e))?;
|
||||||
ProcessedTicket::set_developer_report(&conn, &ticket.id, &developer_report)
|
ProcessedTicket::set_developer_report(&conn, &ticket.id, &developer_report)
|
||||||
|
|
@ -499,12 +559,12 @@ async fn process_ticket(
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start(db: Arc<Mutex<Connection>>, app_handle: AppHandle) {
|
pub fn start(db: Arc<Mutex<Connection>>, app_handle: AppHandle, process_registry: ProcessRegistry) {
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
let mut tick = interval(Duration::from_secs(10));
|
let mut tick = interval(Duration::from_secs(10));
|
||||||
loop {
|
loop {
|
||||||
tick.tick().await;
|
tick.tick().await;
|
||||||
match process_ticket(&db, &app_handle).await {
|
match process_ticket(&db, &app_handle, &process_registry).await {
|
||||||
Ok(true) => {
|
Ok(true) => {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
104
src-tauri/src/services/process_registry.rs
Normal file
104
src-tauri/src/services/process_registry.rs
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use tokio::process::Child;
|
||||||
|
use tokio::sync::Mutex as AsyncMutex;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct ActiveProcess {
|
||||||
|
child: Arc<AsyncMutex<Child>>,
|
||||||
|
cancellation_requested: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveProcess {
|
||||||
|
fn new(child: Arc<AsyncMutex<Child>>, cancellation_requested: Arc<AtomicBool>) -> Self {
|
||||||
|
Self {
|
||||||
|
child,
|
||||||
|
cancellation_requested,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct ProcessRegistry {
|
||||||
|
ticket_processes: Arc<Mutex<HashMap<String, ActiveProcess>>>,
|
||||||
|
task_processes: Arc<Mutex<HashMap<String, ActiveProcess>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProcessRegistry {
|
||||||
|
pub fn register_ticket(
|
||||||
|
&self,
|
||||||
|
ticket_id: &str,
|
||||||
|
child: Arc<AsyncMutex<Child>>,
|
||||||
|
cancellation_requested: Arc<AtomicBool>,
|
||||||
|
) {
|
||||||
|
let mut processes = self
|
||||||
|
.ticket_processes
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|poison| poison.into_inner());
|
||||||
|
processes.insert(
|
||||||
|
ticket_id.to_string(),
|
||||||
|
ActiveProcess::new(child, cancellation_requested),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unregister_ticket(&self, ticket_id: &str) {
|
||||||
|
let mut processes = self
|
||||||
|
.ticket_processes
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|poison| poison.into_inner());
|
||||||
|
processes.remove(ticket_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cancel_ticket(&self, ticket_id: &str) -> bool {
|
||||||
|
self.cancel_process(&self.ticket_processes, ticket_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_task(
|
||||||
|
&self,
|
||||||
|
task_id: &str,
|
||||||
|
child: Arc<AsyncMutex<Child>>,
|
||||||
|
cancellation_requested: Arc<AtomicBool>,
|
||||||
|
) {
|
||||||
|
let mut processes = self
|
||||||
|
.task_processes
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|poison| poison.into_inner());
|
||||||
|
processes.insert(
|
||||||
|
task_id.to_string(),
|
||||||
|
ActiveProcess::new(child, cancellation_requested),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unregister_task(&self, task_id: &str) {
|
||||||
|
let mut processes = self
|
||||||
|
.task_processes
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|poison| poison.into_inner());
|
||||||
|
processes.remove(task_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cancel_task(&self, task_id: &str) -> bool {
|
||||||
|
self.cancel_process(&self.task_processes, task_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn cancel_process(
|
||||||
|
&self,
|
||||||
|
map: &Arc<Mutex<HashMap<String, ActiveProcess>>>,
|
||||||
|
entity_id: &str,
|
||||||
|
) -> bool {
|
||||||
|
let process = {
|
||||||
|
let processes = map.lock().unwrap_or_else(|poison| poison.into_inner());
|
||||||
|
processes.get(entity_id).cloned()
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(process) = process else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
process.cancellation_requested.store(true, Ordering::SeqCst);
|
||||||
|
let mut child = process.child.lock().await;
|
||||||
|
let _ = child.start_kill();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ use crate::models::agent_task::AgentTask;
|
||||||
use crate::models::module::{ProjectModule, MODULE_AGENT_TASK_RUNNER};
|
use crate::models::module::{ProjectModule, MODULE_AGENT_TASK_RUNNER};
|
||||||
use crate::models::project::Project;
|
use crate::models::project::Project;
|
||||||
use crate::services::agent_runtime;
|
use crate::services::agent_runtime;
|
||||||
|
use crate::services::process_registry::ProcessRegistry;
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use tauri::{AppHandle, Emitter};
|
use tauri::{AppHandle, Emitter};
|
||||||
|
|
@ -60,6 +61,7 @@ fn emit_status(
|
||||||
async fn process_next_task(
|
async fn process_next_task(
|
||||||
db: &Arc<Mutex<Connection>>,
|
db: &Arc<Mutex<Connection>>,
|
||||||
app_handle: &AppHandle,
|
app_handle: &AppHandle,
|
||||||
|
process_registry: &ProcessRegistry,
|
||||||
) -> Result<bool, String> {
|
) -> Result<bool, String> {
|
||||||
let next_task = {
|
let next_task = {
|
||||||
let conn = db.lock().map_err(|e| format!("DB lock failed: {}", e))?;
|
let conn = db.lock().map_err(|e| format!("DB lock failed: {}", e))?;
|
||||||
|
|
@ -102,23 +104,45 @@ async fn process_next_task(
|
||||||
|
|
||||||
let prompt = build_task_prompt(&task, &project);
|
let prompt = build_task_prompt(&task, &project);
|
||||||
let args = agent.tool.to_non_interactive_args();
|
let args = agent.tool.to_non_interactive_args();
|
||||||
let result = agent_runtime::run_agent_command(
|
let result = agent_runtime::run_agent_command_for_task(
|
||||||
agent.tool.to_command(),
|
agent.tool.to_command(),
|
||||||
&args,
|
&args,
|
||||||
&prompt,
|
&prompt,
|
||||||
&project.path,
|
&project.path,
|
||||||
900,
|
900,
|
||||||
|
process_registry,
|
||||||
|
&task.id,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(report) => {
|
Ok(report) => {
|
||||||
|
let current = {
|
||||||
|
let conn = db.lock().map_err(|e| format!("DB lock failed: {}", e))?;
|
||||||
|
AgentTask::get_by_id(&conn, &task.id)
|
||||||
|
.map_err(|e| format!("task lookup failed: {}", e))?
|
||||||
|
};
|
||||||
|
if current.status == "cancelled" {
|
||||||
|
emit_status(app_handle, &task.project_id, &task.id, "cancelled", None);
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
let conn = db.lock().map_err(|e| format!("DB lock failed: {}", e))?;
|
let conn = db.lock().map_err(|e| format!("DB lock failed: {}", e))?;
|
||||||
AgentTask::mark_done(&conn, &task.id, &report)
|
AgentTask::mark_done(&conn, &task.id, &report)
|
||||||
.map_err(|e| format!("mark task done failed: {}", e))?;
|
.map_err(|e| format!("mark task done failed: {}", e))?;
|
||||||
emit_status(app_handle, &task.project_id, &task.id, "done", None);
|
emit_status(app_handle, &task.project_id, &task.id, "done", None);
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
|
let current = {
|
||||||
|
let conn = db.lock().map_err(|e| format!("DB lock failed: {}", e))?;
|
||||||
|
AgentTask::get_by_id(&conn, &task.id)
|
||||||
|
.map_err(|e| format!("task lookup failed: {}", e))?
|
||||||
|
};
|
||||||
|
if current.status == "cancelled" {
|
||||||
|
emit_status(app_handle, &task.project_id, &task.id, "cancelled", None);
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
let conn = db.lock().map_err(|e| format!("DB lock failed: {}", e))?;
|
let conn = db.lock().map_err(|e| format!("DB lock failed: {}", e))?;
|
||||||
AgentTask::mark_error(&conn, &task.id, &error)
|
AgentTask::mark_error(&conn, &task.id, &error)
|
||||||
.map_err(|e| format!("mark task error failed: {}", e))?;
|
.map_err(|e| format!("mark task error failed: {}", e))?;
|
||||||
|
|
@ -135,12 +159,12 @@ async fn process_next_task(
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start(db: Arc<Mutex<Connection>>, app_handle: AppHandle) {
|
pub fn start(db: Arc<Mutex<Connection>>, app_handle: AppHandle, process_registry: ProcessRegistry) {
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
let mut tick = interval(Duration::from_secs(8));
|
let mut tick = interval(Duration::from_secs(8));
|
||||||
loop {
|
loop {
|
||||||
tick.tick().await;
|
tick.tick().await;
|
||||||
match process_next_task(&db, &app_handle).await {
|
match process_next_task(&db, &app_handle, &process_registry).await {
|
||||||
Ok(true) => continue,
|
Ok(true) => continue,
|
||||||
Ok(false) => {}
|
Ok(false) => {}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue