mixtape_tools/process/
force_terminate.rs1use crate::prelude::*;
2use crate::process::start_process::SESSION_MANAGER;
3
4#[derive(Debug, Deserialize, JsonSchema)]
6pub struct ForceTerminateInput {
7 pub pid: u32,
9
10 #[serde(default = "default_force")]
12 pub force: bool,
13}
14
15fn default_force() -> bool {
16 true
17}
18
19pub struct ForceTerminateTool;
21
22impl Tool for ForceTerminateTool {
23 type Input = ForceTerminateInput;
24
25 fn name(&self) -> &str {
26 "force_terminate"
27 }
28
29 fn description(&self) -> &str {
30 "Forcefully terminate a process session. Can use either graceful SIGTERM or force SIGKILL."
31 }
32
33 async fn execute(&self, input: Self::Input) -> std::result::Result<ToolResult, ToolError> {
34 let manager = SESSION_MANAGER.lock().await;
35
36 if manager.get_session(input.pid).await.is_none() {
38 return Err(format!("Process {} not found", input.pid).into());
39 }
40
41 manager.terminate(input.pid, input.force).await?;
42
43 let method = if input.force {
44 "force killed"
45 } else {
46 "terminated"
47 };
48
49 Ok(format!("Successfully {} process {}", method, input.pid).into())
50 }
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56 use crate::process::start_process::{StartProcessInput, StartProcessTool};
57
58 #[tokio::test]
59 async fn test_force_terminate_nonexistent() {
60 let tool = ForceTerminateTool;
61
62 let input = ForceTerminateInput {
63 pid: 99999999,
64 force: true,
65 };
66
67 let result = tool.execute(input).await;
68 assert!(result.is_err());
69 assert!(result.unwrap_err().to_string().contains("not found"));
70 }
71
72 #[tokio::test]
73 async fn test_force_terminate_basic() {
74 let start_tool = StartProcessTool;
76 let start_input = StartProcessInput {
77 command: "sleep 10".to_string(),
78 timeout_ms: Some(15000),
79 shell: None,
80 };
81
82 let start_result = start_tool.execute(start_input).await;
83 if start_result.is_err() {
84 return;
86 }
87
88 let start_output = start_result.unwrap().as_text();
89 if let Some(pid_line) = start_output.lines().find(|l| l.contains("PID:")) {
91 if let Some(pid_str) = pid_line.split(':').nth(1) {
92 if let Ok(pid) = pid_str.trim().parse::<u32>() {
93 let term_tool = ForceTerminateTool;
95 let term_input = ForceTerminateInput { pid, force: true };
96
97 let result = term_tool.execute(term_input).await;
98 assert!(result.is_ok());
99 let output = result.unwrap().as_text();
100 assert!(output.contains("Successfully"));
101 return;
102 }
103 }
104 }
105 }
106
107 #[tokio::test]
108 async fn test_force_terminate_graceful() {
109 let start_tool = StartProcessTool;
110 let start_input = StartProcessInput {
111 command: "sleep 5".to_string(),
112 timeout_ms: Some(10000),
113 shell: None,
114 };
115
116 let start_result = start_tool.execute(start_input).await;
117 if start_result.is_err() {
118 return;
119 }
120
121 let start_output = start_result.unwrap().as_text();
122 if let Some(pid_line) = start_output.lines().find(|l| l.contains("PID:")) {
123 if let Some(pid_str) = pid_line.split(':').nth(1) {
124 if let Ok(pid) = pid_str.trim().parse::<u32>() {
125 let term_tool = ForceTerminateTool;
127 let term_input = ForceTerminateInput { pid, force: false };
128
129 let result = term_tool.execute(term_input).await;
130 assert!(result.is_ok());
131 return;
132 }
133 }
134 }
135 }
136
137 #[test]
140 fn test_default_force() {
141 assert!(default_force());
142 }
143
144 #[test]
147 fn test_tool_name() {
148 let tool = ForceTerminateTool;
149 assert_eq!(tool.name(), "force_terminate");
150 }
151
152 #[test]
153 fn test_tool_description() {
154 let tool = ForceTerminateTool;
155 assert!(!tool.description().is_empty());
156 assert!(
157 tool.description().contains("terminate") || tool.description().contains("Terminate")
158 );
159 }
160
161 #[test]
164 fn test_force_terminate_input_debug() {
165 let input = ForceTerminateInput {
166 pid: 12345,
167 force: true,
168 };
169 let debug_str = format!("{:?}", input);
170 assert!(debug_str.contains("12345"));
171 assert!(debug_str.contains("true"));
172 }
173
174 #[tokio::test]
177 async fn test_force_terminate_output_message_force() {
178 let start_tool = StartProcessTool;
179 let start_input = StartProcessInput {
180 command: "sleep 10".to_string(),
181 timeout_ms: Some(15000),
182 shell: None,
183 };
184
185 let start_result = start_tool.execute(start_input).await;
186 if start_result.is_err() {
187 return;
188 }
189
190 let start_output = start_result.unwrap().as_text();
191 if let Some(pid_line) = start_output.lines().find(|l| l.contains("PID:")) {
192 if let Some(pid_str) = pid_line.split(':').nth(1) {
193 if let Ok(pid) = pid_str.trim().parse::<u32>() {
194 let term_tool = ForceTerminateTool;
195 let term_input = ForceTerminateInput { pid, force: true };
196
197 let result = term_tool.execute(term_input).await;
198 if let Ok(output) = result {
199 assert!(output.as_text().contains("force killed"));
201 }
202 return;
203 }
204 }
205 }
206 }
207
208 #[tokio::test]
209 async fn test_force_terminate_output_message_graceful() {
210 let start_tool = StartProcessTool;
211 let start_input = StartProcessInput {
212 command: "sleep 10".to_string(),
213 timeout_ms: Some(15000),
214 shell: None,
215 };
216
217 let start_result = start_tool.execute(start_input).await;
218 if start_result.is_err() {
219 return;
220 }
221
222 let start_output = start_result.unwrap().as_text();
223 if let Some(pid_line) = start_output.lines().find(|l| l.contains("PID:")) {
224 if let Some(pid_str) = pid_line.split(':').nth(1) {
225 if let Ok(pid) = pid_str.trim().parse::<u32>() {
226 let term_tool = ForceTerminateTool;
227 let term_input = ForceTerminateInput { pid, force: false };
228
229 let result = term_tool.execute(term_input).await;
230 if let Ok(output) = result {
231 assert!(output.as_text().contains("terminated"));
233 }
234 return;
235 }
236 }
237 }
238 }
239}