1use std::io::{self, BufRead, Write};
21use std::sync::mpsc::{self, RecvTimeoutError};
22use std::thread;
23use std::time::{Duration, Instant};
24
25use serde::{Deserialize, Serialize};
26
27use crate::snapshot::Snapshot;
28use crate::source::{DataSource, MIN_POLL_INTERVAL, ProcessSignal};
29
30pub const PROTOCOL_VERSION: u32 = 1;
34
35pub const FEATURE_WATCH_PROCESSES: &str = "watch_processes";
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42#[serde(tag = "type", rename_all = "snake_case")]
43pub enum AgentMessage {
44 Hello {
46 protocol: u32,
48 version: String,
50 #[serde(default)]
53 features: Vec<String>,
54 },
55 Snapshot {
57 snapshot: Box<Snapshot>,
60 },
61 SignalResult {
63 pid: u32,
64 signal: ProcessSignal,
65 error: Option<String>,
67 },
68 Error { message: String },
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74#[serde(tag = "type", rename_all = "snake_case")]
75pub enum ClientMessage {
76 SetInterval { ms: u64 },
78 SetProcessLimit { limit: Option<usize> },
81 WatchProcesses { pids: Vec<u32> },
85 Signal { pid: u32, signal: ProcessSignal },
87}
88
89pub fn write_message<W: Write, M: Serialize>(writer: &mut W, message: &M) -> io::Result<()> {
91 serde_json::to_writer(&mut *writer, message)?;
92 writer.write_all(b"\n")?;
93 writer.flush()
94}
95
96enum Input {
98 Command(ClientMessage),
99 Invalid(String),
100 Closed,
101}
102
103pub fn run_agent<S, R, W>(
109 mut source: S,
110 mut interval: Duration,
111 input: R,
112 mut output: W,
113) -> io::Result<()>
114where
115 S: DataSource,
116 R: BufRead + Send + 'static,
117 W: Write,
118{
119 let (tx, rx) = mpsc::channel();
120 thread::Builder::new()
121 .name("rigtop-agent-input".to_owned())
122 .spawn(move || {
123 for line in input.lines() {
124 let Ok(line) = line else { break };
125 if line.trim().is_empty() {
126 continue;
127 }
128 let message = match serde_json::from_str::<ClientMessage>(&line) {
129 Ok(command) => Input::Command(command),
130 Err(err) => Input::Invalid(format!("invalid command {line:?}: {err}")),
131 };
132 if tx.send(message).is_err() {
133 return;
134 }
135 }
136 let _ = tx.send(Input::Closed);
137 })?;
138
139 let hello = AgentMessage::Hello {
140 protocol: PROTOCOL_VERSION,
141 version: env!("CARGO_PKG_VERSION").to_owned(),
142 features: vec![FEATURE_WATCH_PROCESSES.to_owned()],
143 };
144 if write_message(&mut output, &hello).is_err() {
146 return Ok(());
147 }
148
149 let mut process_limit: Option<usize> = None;
150 let mut next_poll = Instant::now() + MIN_POLL_INTERVAL;
151 loop {
152 let timeout = next_poll.saturating_duration_since(Instant::now());
153 let reply = match rx.recv_timeout(timeout) {
154 Ok(Input::Command(ClientMessage::SetInterval { ms })) => {
155 interval = Duration::from_millis(ms).max(MIN_POLL_INTERVAL);
156 next_poll = Instant::now() + interval;
157 None
158 }
159 Ok(Input::Command(ClientMessage::SetProcessLimit { limit })) => {
160 process_limit = limit;
161 source.set_process_limit(limit);
163 None
164 }
165 Ok(Input::Command(ClientMessage::WatchProcesses { pids })) => {
166 source.watch_processes(pids);
167 None
168 }
169 Ok(Input::Command(ClientMessage::Signal { pid, signal })) => {
170 let error = source
171 .signal_process(pid, signal)
172 .err()
173 .map(|e| e.to_string());
174 Some(AgentMessage::SignalResult { pid, signal, error })
175 }
176 Ok(Input::Invalid(message)) => Some(AgentMessage::Error { message }),
177 Ok(Input::Closed) | Err(RecvTimeoutError::Disconnected) => return Ok(()),
178 Err(RecvTimeoutError::Timeout) => {
179 let message = match source.poll() {
180 Ok(mut snapshot) => {
181 if let Some(limit) = process_limit {
182 snapshot.truncate_processes(limit);
183 }
184 AgentMessage::Snapshot {
185 snapshot: Box::new(snapshot),
186 }
187 }
188 Err(err) => AgentMessage::Error {
189 message: err.to_string(),
190 },
191 };
192 next_poll = (next_poll + interval).max(Instant::now() + MIN_POLL_INTERVAL);
193 Some(message)
194 }
195 };
196 if let Some(message) = reply
197 && write_message(&mut output, &message).is_err()
198 {
199 return Ok(());
200 }
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use crate::snapshot::ProcessInfo;
208 use crate::source::SourceError;
209 use std::io::Cursor;
210 use std::sync::{Arc, Mutex};
211
212 #[derive(Default)]
213 struct FakeSource {
214 watched_pids: Vec<u32>,
215 }
216
217 impl DataSource for FakeSource {
218 fn poll(&mut self) -> Result<Snapshot, SourceError> {
219 Ok(Snapshot {
220 watched_pids: self.watched_pids.clone(),
221 processes: (1..=20)
222 .map(|pid| ProcessInfo {
223 pid,
224 cpu_percent: pid as f32,
225 ..Default::default()
226 })
227 .collect(),
228 process_count: 20,
229 ..Default::default()
230 })
231 }
232
233 fn signal_process(&mut self, pid: u32, _signal: ProcessSignal) -> Result<(), SourceError> {
234 if pid == 1 {
235 Err(SourceError::Disconnected("denied".to_owned()))
236 } else {
237 Ok(())
238 }
239 }
240
241 fn watch_processes(&mut self, pids: Vec<u32>) {
242 self.watched_pids = pids;
243 }
244 }
245
246 #[derive(Clone, Default)]
248 struct SharedBuffer(Arc<Mutex<Vec<u8>>>);
249
250 impl Write for SharedBuffer {
251 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
252 self.0.lock().unwrap().write(buf)
253 }
254 fn flush(&mut self) -> io::Result<()> {
255 Ok(())
256 }
257 }
258
259 #[test]
260 fn messages_round_trip_as_tagged_json() {
261 let command = ClientMessage::Signal {
262 pid: 42,
263 signal: ProcessSignal::Kill,
264 };
265 let line = serde_json::to_string(&command).unwrap();
266 assert_eq!(line, r#"{"type":"signal","pid":42,"signal":"Kill"}"#);
267 assert_eq!(
268 serde_json::from_str::<ClientMessage>(&line).unwrap(),
269 command
270 );
271 let limit: ClientMessage =
272 serde_json::from_str(r#"{"type":"set_process_limit","limit":null}"#).unwrap();
273 assert_eq!(limit, ClientMessage::SetProcessLimit { limit: None });
274 }
275
276 #[test]
277 fn agent_answers_commands_and_stops_at_end_of_input() {
278 let input = Cursor::new(
281 "{\"type\":\"set_process_limit\",\"limit\":3}\n\
282 {\"type\":\"signal\",\"pid\":1,\"signal\":\"Terminate\"}\n\
283 not json\n",
284 );
285 let output = SharedBuffer::default();
286 run_agent(
287 FakeSource::default(),
288 Duration::from_secs(1),
289 input,
290 output.clone(),
291 )
292 .unwrap();
293
294 let text = String::from_utf8(output.0.lock().unwrap().clone()).unwrap();
295 let messages: Vec<AgentMessage> = text
296 .lines()
297 .map(|l| serde_json::from_str(l).unwrap())
298 .collect();
299 assert!(matches!(
300 &messages[0],
301 AgentMessage::Hello {
302 protocol: PROTOCOL_VERSION,
303 features,
304 ..
305 } if features.iter().any(|f| f == FEATURE_WATCH_PROCESSES)
306 ));
307 assert!(matches!(
308 &messages[1],
309 AgentMessage::SignalResult { pid: 1, error: Some(e), .. } if e.contains("denied")
310 ));
311 assert!(
312 matches!(&messages[2], AgentMessage::Error { message } if message.contains("invalid command"))
313 );
314 assert_eq!(
315 messages.len(),
316 3,
317 "no snapshot before the first interval: {text}"
318 );
319 }
320
321 #[test]
322 fn snapshots_respect_the_process_limit() {
323 let (reader, mut writer) = std::io::pipe().unwrap();
325 let output = SharedBuffer::default();
326 let out = output.clone();
327 let agent = thread::spawn(move || {
328 run_agent(
329 FakeSource::default(),
330 MIN_POLL_INTERVAL,
331 io::BufReader::new(reader),
332 out,
333 )
334 });
335 writeln!(writer, r#"{{"type":"set_process_limit","limit":3}}"#).unwrap();
336 writeln!(writer, r#"{{"type":"watch_processes","pids":[2]}}"#).unwrap();
338 let deadline = Instant::now() + Duration::from_secs(5);
339 let snapshot = loop {
340 let text = String::from_utf8(output.0.lock().unwrap().clone()).unwrap();
341 let found = text.lines().find_map(|l| match serde_json::from_str(l) {
342 Ok(AgentMessage::Snapshot { snapshot }) => Some(snapshot),
343 _ => None,
344 });
345 if let Some(snapshot) = found {
346 break snapshot;
347 }
348 assert!(Instant::now() < deadline, "no snapshot: {text}");
349 thread::sleep(Duration::from_millis(20));
350 };
351 let pids: Vec<u32> = snapshot.processes.iter().map(|p| p.pid).collect();
352 assert_eq!(pids, [20, 19, 18, 2], "top CPU first, then watched ones");
353 assert_eq!(snapshot.watched_pids, [2]);
354 assert_eq!(snapshot.process_count, 20);
355 drop(writer);
356 agent.join().unwrap().unwrap();
357 }
358}