Skip to main content

mixtape_tools/process/
force_terminate.rs

1use crate::prelude::*;
2use crate::process::start_process::SESSION_MANAGER;
3
4/// Input for forcefully terminating a process
5#[derive(Debug, Deserialize, JsonSchema)]
6pub struct ForceTerminateInput {
7    /// Process ID to terminate
8    pub pid: u32,
9
10    /// Use force kill instead of graceful termination (default: true)
11    #[serde(default = "default_force")]
12    pub force: bool,
13}
14
15fn default_force() -> bool {
16    true
17}
18
19/// Tool for forcefully terminating a process session
20pub 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        // Check if session exists
37        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        // Start a long-running process
75        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            // Skip if process creation fails
85            return;
86        }
87
88        let start_output = start_result.unwrap().as_text();
89        // Extract PID
90        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                    // Terminate it
94                    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                    // Graceful terminate
126                    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    // ==================== default value tests ====================
138
139    #[test]
140    fn test_default_force() {
141        assert!(default_force());
142    }
143
144    // ==================== Tool metadata tests ====================
145
146    #[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    // ==================== Input struct tests ====================
162
163    #[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    // ==================== Output message tests ====================
175
176    #[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                        // Force kill message
200                        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                        // Graceful terminate message
232                        assert!(output.as_text().contains("terminated"));
233                    }
234                    return;
235                }
236            }
237        }
238    }
239}