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