1use serde::{Deserialize, Serialize};
7
8use crate::systemd::detect_systemd_unit;
9use network_inspector::tcp;
10
11#[derive(Debug, Serialize, Deserialize, Clone)]
13pub struct SignalImpact {
14 pub active_tcp_connections: usize,
16 pub child_process_count: usize,
18 pub has_file_locks: bool,
20 pub systemd_unit: Option<String>,
22 pub recommendation: String,
24 pub prefer_graceful: bool,
26}
27
28pub fn analyze_impact(pid: i32) -> anyhow::Result<SignalImpact> {
30 let active_tcp_connections = count_tcp_connections(pid);
31 let child_process_count = count_children(pid);
32 let has_file_locks = check_file_locks(pid);
33 let systemd_unit = detect_systemd_unit(pid);
34
35 let (recommendation, prefer_graceful) = build_recommendation(
36 active_tcp_connections,
37 child_process_count,
38 has_file_locks,
39 &systemd_unit,
40 );
41
42 Ok(SignalImpact {
43 active_tcp_connections,
44 child_process_count,
45 has_file_locks,
46 systemd_unit,
47 recommendation,
48 prefer_graceful,
49 })
50}
51
52fn count_tcp_connections(pid: i32) -> usize {
55 let inodes = tcp::process_socket_inodes(pid);
56 if inodes.is_empty() {
57 return 0;
58 }
59 let mut count = 0usize;
60 for path in &["/proc/net/tcp", "/proc/net/tcp6"] {
61 if let Ok(raw) = std::fs::read_to_string(path) {
62 for line in raw.lines().skip(1) {
63 let fields: Vec<&str> = line.split_whitespace().collect();
64 if fields.len() < 10 {
65 continue;
66 }
67 let inode: u64 = fields[9].parse().unwrap_or(0);
68 if inodes.contains(&inode) {
69 let state = u8::from_str_radix(fields[3], 16).unwrap_or(0);
71 if state == 0x01 {
72 count += 1;
73 }
74 }
75 }
76 }
77 }
78 count
79}
80
81fn count_children(pid: i32) -> usize {
84 let raw = match std::fs::read_to_string(format!("/proc/{}/task/{}/children", pid, pid)) {
85 Ok(r) => r,
86 Err(_) => {
87 return count_children_fallback(pid);
89 }
90 };
91 raw.split_whitespace().count()
92}
93
94fn count_children_fallback(parent_pid: i32) -> usize {
95 let mut count = 0usize;
96 if let Ok(entries) = std::fs::read_dir("/proc") {
97 for entry in entries.flatten() {
98 let name = entry.file_name();
99 let s = name.to_string_lossy();
100 if let Ok(p) = s.parse::<i32>() {
101 if let Ok(stat) = std::fs::read_to_string(format!("/proc/{}/stat", p)) {
102 let after = stat.rfind(')').map(|i| &stat[i + 2..]).unwrap_or("");
103 let fields: Vec<&str> = after.split_whitespace().collect();
104 let ppid: i32 = fields.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
105 if ppid == parent_pid {
106 count += 1;
107 }
108 }
109 }
110 }
111 }
112 count
113}
114
115fn check_file_locks(pid: i32) -> bool {
118 if let Ok(raw) = std::fs::read_to_string("/proc/locks") {
120 let pid_str = format!("{}", pid);
121 for line in raw.lines() {
122 let fields: Vec<&str> = line.split_whitespace().collect();
124 for &idx in &[4usize, 5usize] {
126 if fields.get(idx).copied() == Some(pid_str.as_str()) {
127 if fields.get(3).map(|s| s.contains("WRITE")).unwrap_or(false) {
129 return true;
130 }
131 }
132 }
133 }
134 }
135 false
136}
137
138fn build_recommendation(
141 tcp: usize,
142 children: usize,
143 locks: bool,
144 unit: &Option<String>,
145) -> (String, bool) {
146 let mut points = Vec::new();
147 let mut prefer_graceful = false;
148
149 if tcp > 0 {
150 points.push(format!(
151 "{} active TCP connection(s) will be abruptly terminated by a hard kill",
152 tcp
153 ));
154 prefer_graceful = true;
155 }
156 if children > 0 {
157 points.push(format!(
158 "{} child process(es) will be orphaned or killed (depending on signal)",
159 children
160 ));
161 }
162 if locks {
163 points.push(
164 "process holds exclusive file lock(s) — hard kill may leave stale locks".to_string(),
165 );
166 prefer_graceful = true;
167 }
168 if let Some(unit) = unit {
169 points.push(format!(
170 "process is managed by systemd unit '{}' — consider 'systemctl stop/restart' instead",
171 unit
172 ));
173 prefer_graceful = true;
174 }
175
176 if points.is_empty() {
177 (
178 "Process appears safe to terminate with SIGKILL.".to_string(),
179 false,
180 )
181 } else {
182 let rec = format!(
183 "{}{}",
184 points.join(". "),
185 if prefer_graceful {
186 ". Graceful stop (SIGTERM) is recommended."
187 } else {
188 "."
189 }
190 );
191 (rec, prefer_graceful)
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn recommendation_no_risks() {
201 let (msg, graceful) = build_recommendation(0, 0, false, &None);
202 assert!(!graceful);
203 assert!(msg.contains("safe"));
204 }
205
206 #[test]
207 fn recommendation_with_tcp() {
208 let (msg, graceful) = build_recommendation(5, 0, false, &None);
209 assert!(graceful);
210 assert!(msg.contains("TCP"));
211 }
212
213 #[test]
214 fn recommendation_with_systemd() {
215 let (msg, graceful) = build_recommendation(0, 0, false, &Some("nginx.service".to_string()));
216 assert!(graceful);
217 assert!(msg.contains("systemd"));
218 }
219}