aft/bash_background/
mod.rs1pub mod buffer;
4pub mod persistence;
5pub mod process;
6pub mod registry;
7pub mod watchdog;
8
9use crate::context::AppContext;
10use crate::protocol::Response;
11use serde::{Deserialize, Serialize};
12use serde_json::json;
13use std::collections::HashMap;
14use std::path::PathBuf;
15use std::time::Duration;
16
17pub use registry::{BgCompletion, BgTaskRegistry};
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct BgTaskInfo {
21 pub task_id: String,
22 pub status: BgTaskStatus,
23 pub command: String,
24 pub started_at: u64,
25 pub duration_ms: Option<u64>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
29#[serde(rename_all = "snake_case")]
30pub enum BgTaskStatus {
31 Starting,
32 Running,
33 Killing,
34 Completed,
35 Failed,
36 Killed,
37 TimedOut,
38}
39
40impl BgTaskStatus {
41 pub fn is_terminal(&self) -> bool {
42 matches!(
43 self,
44 BgTaskStatus::Completed
45 | BgTaskStatus::Failed
46 | BgTaskStatus::Killed
47 | BgTaskStatus::TimedOut
48 )
49 }
50}
51
52#[allow(clippy::too_many_arguments)]
54pub fn spawn(
55 request_id: &str,
56 session_id: &str,
57 command: &str,
58 workdir: Option<PathBuf>,
59 env: Option<HashMap<String, String>>,
60 timeout_ms: Option<u64>,
61 ctx: &AppContext,
62 require_background_flag: bool,
63 notify_on_completion: bool,
64 compressed: bool,
65) -> Response {
66 if require_background_flag && !ctx.config().experimental_bash_background {
67 return Response::error(
68 request_id,
69 "feature_disabled",
70 "background bash is disabled; set `experimental.bash.background: true` in aft.jsonc",
71 );
72 }
73
74 let workdir = workdir.unwrap_or_else(|| {
75 ctx.config().project_root.clone().unwrap_or_else(|| {
76 std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."))
77 })
78 });
79 let storage_dir = storage_dir(ctx.config().storage_dir.as_deref());
80 let max_running = ctx.config().max_background_bash_tasks;
81 let timeout = timeout_ms.map(Duration::from_millis);
82 let project_root = ctx
83 .config()
84 .project_root
85 .clone()
86 .or_else(|| std::env::current_dir().ok())
87 .and_then(|path| std::fs::canonicalize(&path).ok().or(Some(path)));
88
89 match ctx.bash_background().spawn(
90 command,
91 session_id.to_string(),
92 workdir,
93 env.unwrap_or_default(),
94 timeout,
95 storage_dir,
96 max_running,
97 notify_on_completion,
98 compressed,
99 project_root,
100 ) {
101 Ok(task_id) => Response::success(
102 request_id,
103 json!({
104 "task_id": task_id,
105 "status": BgTaskStatus::Running,
106 }),
107 ),
108 Err(message) if message.contains("limit exceeded") => {
109 Response::error(request_id, "background_task_limit_exceeded", message)
110 }
111 Err(message) => Response::error(request_id, "execution_failed", message),
112 }
113}
114
115pub fn storage_dir(configured: Option<&std::path::Path>) -> PathBuf {
116 if let Some(dir) = configured {
117 return dir.to_path_buf();
118 }
119 if let Some(dir) = std::env::var_os("AFT_CACHE_DIR") {
120 return PathBuf::from(dir).join("aft");
121 }
122 let home = std::env::var_os("HOME")
131 .or_else(|| std::env::var_os("USERPROFILE"))
132 .map(PathBuf::from)
133 .unwrap_or_else(std::env::temp_dir);
134 home.join(".cache").join("aft")
135}