coding_agent_search/daemon/
resource.rs1#[cfg(target_os = "linux")]
9use std::fs;
10#[cfg(target_os = "linux")]
11use std::process::Command;
12
13use tracing::debug;
14#[cfg(target_os = "linux")]
15use tracing::warn;
16
17#[cfg(target_os = "linux")]
19mod posix {
20 use std::ffi::{c_int, c_long, c_uint};
21 pub const _SC_PAGESIZE: c_int = 30;
22 pub const PRIO_PROCESS: c_int = 0;
23 unsafe extern "C" {
24 pub fn sysconf(name: c_int) -> c_long;
25 pub fn setpriority(which: c_int, who: c_uint, prio: c_int) -> c_int;
26 }
27}
28
29#[derive(Debug, Default)]
31pub struct ResourceMonitor {
32 #[cfg(target_os = "linux")]
34 pid: u32,
35}
36
37impl ResourceMonitor {
38 pub fn new() -> Self {
40 Self {
41 #[cfg(target_os = "linux")]
42 pid: std::process::id(),
43 }
44 }
45
46 pub fn memory_usage(&self) -> u64 {
50 #[cfg(target_os = "linux")]
51 {
52 self.linux_memory_usage()
53 }
54 #[cfg(not(target_os = "linux"))]
55 {
56 0
57 }
58 }
59
60 #[cfg(target_os = "linux")]
62 fn linux_memory_usage(&self) -> u64 {
63 let page_size = Self::page_size();
66
67 match fs::read_to_string("/proc/self/statm") {
68 Ok(content) => {
69 let parts: Vec<&str> = content.split_whitespace().collect();
70 if parts.len() >= 2 {
71 if let Ok(pages) = parts[1].parse::<u64>() {
73 return pages * page_size;
74 }
75 }
76 0
77 }
78 Err(e) => {
79 debug!(error = %e, "Failed to read /proc/self/statm");
80 0
81 }
82 }
83 }
84
85 #[cfg(target_os = "linux")]
87 fn page_size() -> u64 {
88 let raw = unsafe { posix::sysconf(posix::_SC_PAGESIZE) };
90 if raw > 0 { raw as u64 } else { 4096 }
91 }
92
93 pub fn apply_nice(&self, nice_value: i32) -> bool {
98 #[cfg(target_os = "linux")]
99 {
100 if !(-20..=19).contains(&nice_value) {
101 warn!(
102 nice = nice_value,
103 "Refusing out-of-range nice value (valid range: -20..=19)"
104 );
105 return false;
106 }
107
108 let result = unsafe {
111 posix::setpriority(
112 posix::PRIO_PROCESS,
113 self.pid as std::ffi::c_uint,
114 nice_value,
115 )
116 };
117 if result != 0 {
118 let err = std::io::Error::last_os_error();
119 warn!(nice = nice_value, error = %err, "Failed to set nice value");
120 return false;
121 }
122
123 debug!(
124 nice = nice_value,
125 pid = self.pid,
126 "Applied absolute nice value"
127 );
128 true
129 }
130
131 #[cfg(not(target_os = "linux"))]
132 {
133 debug!(nice = nice_value, "nice not supported on this platform");
134 let _ = nice_value;
135 false
136 }
137 }
138
139 pub fn apply_ionice(&self, class: u32) -> bool {
149 #[cfg(target_os = "linux")]
150 {
151 if class > 3 {
152 warn!(
153 class = class,
154 "Refusing unsupported ionice class (valid classes: 0..=3)"
155 );
156 return false;
157 }
158 let class_str = class.to_string();
159
160 match Command::new("ionice")
162 .args(["-c", &class_str, "-p", &self.pid.to_string()])
163 .output()
164 {
165 Ok(output) => {
166 if output.status.success() {
167 debug!(class = class, pid = self.pid, "Applied ionice class");
168 true
169 } else {
170 let stderr = String::from_utf8_lossy(&output.stderr);
171 warn!(
172 class = class,
173 error = %stderr,
174 "ionice command failed"
175 );
176 false
177 }
178 }
179 Err(e) => {
180 warn!(error = %e, "ionice command not available");
181 false
182 }
183 }
184 }
185
186 #[cfg(not(target_os = "linux"))]
187 {
188 debug!(class = class, "ionice not supported on this platform");
189 let _ = class;
190 false
191 }
192 }
193
194 pub fn memory_usage_human(&self) -> String {
196 let bytes = self.memory_usage();
197 if bytes == 0 {
198 return "unknown".to_string();
199 }
200
201 const KB: u64 = 1024;
202 const MB: u64 = KB * 1024;
203 const GB: u64 = MB * 1024;
204
205 if bytes >= GB {
206 format!("{:.1} GB", bytes as f64 / GB as f64)
207 } else if bytes >= MB {
208 format!("{:.1} MB", bytes as f64 / MB as f64)
209 } else if bytes >= KB {
210 format!("{:.1} KB", bytes as f64 / KB as f64)
211 } else {
212 format!("{} B", bytes)
213 }
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn test_resource_monitor_creation() {
223 let monitor = ResourceMonitor::new();
224 #[cfg(target_os = "linux")]
225 assert!(monitor.pid > 0);
226 }
227
228 #[test]
229 fn test_memory_usage() {
230 let monitor = ResourceMonitor::new();
231 let mem = monitor.memory_usage();
232
233 #[cfg(target_os = "linux")]
235 assert!(mem > 0, "Memory usage should be non-zero on Linux");
236
237 #[cfg(not(target_os = "linux"))]
239 assert_eq!(mem, 0);
240 }
241
242 #[test]
243 fn test_memory_usage_human() {
244 let monitor = ResourceMonitor::new();
245 let human = monitor.memory_usage_human();
246
247 assert!(!human.is_empty());
249
250 #[cfg(target_os = "linux")]
251 {
252 assert!(
254 human.contains("KB") || human.contains("MB") || human.contains("GB"),
255 "Memory string should contain unit: {}",
256 human
257 );
258 }
259 }
260
261 #[test]
262 fn test_apply_nice_range() {
263 let monitor = ResourceMonitor::new();
264
265 #[cfg(target_os = "linux")]
268 {
269 let result = monitor.apply_nice(19);
271 let _ = result;
273 }
274
275 #[cfg(not(target_os = "linux"))]
276 {
277 assert!(!monitor.apply_nice(10));
278 }
279 }
280
281 #[test]
282 fn test_apply_ionice() {
283 let monitor = ResourceMonitor::new();
284
285 #[cfg(target_os = "linux")]
286 {
287 let result = monitor.apply_ionice(2);
289 let _ = result;
291
292 let result = monitor.apply_ionice(3);
294 let _ = result;
295 }
296
297 #[cfg(not(target_os = "linux"))]
298 {
299 assert!(!monitor.apply_ionice(2));
300 }
301 }
302
303 #[test]
304 fn test_page_size() {
305 #[cfg(target_os = "linux")]
306 {
307 let size = ResourceMonitor::page_size();
308 assert!(size > 0);
309 assert!(size.is_power_of_two());
310 }
311 }
312
313 #[test]
314 fn test_apply_nice_rejects_out_of_range() {
315 let monitor = ResourceMonitor::new();
316 assert!(!monitor.apply_nice(20));
317 assert!(!monitor.apply_nice(-21));
318 }
319
320 #[test]
321 fn test_apply_ionice_rejects_invalid_class() {
322 let monitor = ResourceMonitor::new();
323 assert!(!monitor.apply_ionice(4));
324 }
325}