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